Compare commits
398 Commits
v0.0.7-pre
...
mek-10.0-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d20b1299 | ||
|
|
88c34d8bca | ||
|
|
3267e7ff13 | ||
|
|
6686d8ea62 | ||
|
|
c47e0044b1 | ||
|
|
265368f9b2 | ||
|
|
70d9da847e | ||
|
|
1bf21564f9 | ||
|
|
cd6bb7376d | ||
|
|
e0ab2ade89 | ||
|
|
10c53ac4b3 | ||
|
|
d9be5ccb47 | ||
|
|
c14fc048a1 | ||
|
|
4275c9d408 | ||
|
|
98c826e762 | ||
|
|
dcf275784c | ||
|
|
6f3405949d | ||
|
|
33695b2ed6 | ||
|
|
350370a084 | ||
|
|
17954ef3d0 | ||
|
|
c5ba95449f | ||
|
|
3621f53c45 | ||
|
|
e084ae1eea | ||
|
|
49605e5966 | ||
|
|
0f6b3fdd98 | ||
|
|
c2ac7fc973 | ||
|
|
b53d2d6694 | ||
|
|
117784500a | ||
|
|
397e311f1b | ||
|
|
e456d34468 | ||
|
|
4359cc3e63 | ||
|
|
473763fd27 | ||
|
|
621adbbcbc | ||
|
|
564b89d19c | ||
|
|
17fce01ff5 | ||
|
|
f36b0c7e37 | ||
|
|
5a8bba5108 | ||
|
|
c3f7407689 | ||
|
|
d38e5ca5ec | ||
|
|
eadf5c488a | ||
|
|
c985e90ec3 | ||
|
|
c80d861b28 | ||
|
|
395c1ff9ce | ||
|
|
8dac59fba4 | ||
|
|
7f011369c4 | ||
|
|
3c2f631451 | ||
|
|
02c3c5c53c | ||
|
|
252c48a02c | ||
|
|
6b23a32744 | ||
|
|
826114e5bf | ||
|
|
17dd35e6de | ||
|
|
42c2b1bda1 | ||
|
|
f5c703a8b3 | ||
|
|
2918608326 | ||
|
|
14b24678f9 | ||
|
|
f4f36b020b | ||
|
|
f1a50990f2 | ||
|
|
01a364b5cf | ||
|
|
fc14141321 | ||
|
|
9b21a971fe | ||
|
|
1afafba501 | ||
|
|
d6a201a45f | ||
|
|
41cc6b9acc | ||
|
|
2aedc015c8 | ||
|
|
c3d6d900a1 | ||
|
|
525dedb830 | ||
|
|
88bf4d5653 | ||
|
|
6643c7e6ed | ||
|
|
bd1ab11686 | ||
|
|
8704d845bd | ||
|
|
6f61203db3 | ||
|
|
5a96818c97 | ||
|
|
b25ebdf959 | ||
|
|
4b60c038f4 | ||
|
|
ea17ba41fe | ||
|
|
39672fedb4 | ||
|
|
1444008479 | ||
|
|
409e8083a7 | ||
|
|
335e0f5ee9 | ||
|
|
9bd220cbb2 | ||
|
|
33159bc677 | ||
|
|
bd33240515 | ||
|
|
f6708ca988 | ||
|
|
ed0982a832 | ||
|
|
7ad115bc03 | ||
|
|
3048fbed8b | ||
|
|
35c408883a | ||
|
|
20a1fab611 | ||
|
|
ef73c52417 | ||
|
|
01caf3d914 | ||
|
|
f32cdf5563 | ||
|
|
1188d2f7df | ||
|
|
e137953f93 | ||
|
|
316b255a04 | ||
|
|
6397f29d4f | ||
|
|
47599b8ff6 | ||
|
|
e54d5b3d85 | ||
|
|
cf6f0e3153 | ||
|
|
d3f28a6882 | ||
|
|
15595ca81b | ||
|
|
5a3897572d | ||
|
|
e4b7f807fe | ||
|
|
9bd2229e27 | ||
|
|
27038f64f7 | ||
|
|
6980e73658 | ||
|
|
ea9e9288f7 | ||
|
|
7f007e032d | ||
|
|
971657c3d2 | ||
|
|
b628472d81 | ||
|
|
2e4a533148 | ||
|
|
13513a9ce6 | ||
|
|
3593493c98 | ||
|
|
7dbc5594b0 | ||
|
|
89437b2be9 | ||
|
|
4488a0594f | ||
|
|
3004902ce5 | ||
|
|
0950fc045d | ||
|
|
dc867095fd | ||
|
|
1fa87132d6 | ||
|
|
11e4d89b1d | ||
|
|
307883e6e7 | ||
|
|
1dad4bcf77 | ||
|
|
bc844d21bd | ||
|
|
d8bbe4b459 | ||
|
|
6f645579f8 | ||
|
|
ac607f9dc6 | ||
|
|
15bc816d7e | ||
|
|
254e85f3ed | ||
|
|
9d107da8d9 | ||
|
|
b99f57e480 | ||
|
|
2ac9bab92e | ||
|
|
29c4c39d23 | ||
|
|
8002698dd0 | ||
|
|
ce227a175a | ||
|
|
8ea75b9501 | ||
|
|
285026c1fa | ||
|
|
8b307ea030 | ||
|
|
b75d482f4a | ||
|
|
ebcc911b81 | ||
|
|
0bc0decbf2 | ||
|
|
1c819779c7 | ||
|
|
d6c8eb4d56 | ||
|
|
81345f5325 | ||
|
|
f0c97e8b70 | ||
|
|
5068e47590 | ||
|
|
c764506999 | ||
|
|
6d97d45227 | ||
|
|
e443beec19 | ||
|
|
0f7e77b0cb | ||
|
|
27a86cc893 | ||
|
|
07574aa116 | ||
|
|
dcb517d1cb | ||
|
|
1242c5a81c | ||
|
|
5cba8ff9f1 | ||
|
|
fc7b83a18a | ||
|
|
3bb95eb441 | ||
|
|
341df1a739 | ||
|
|
ccc5220ca8 | ||
|
|
e52b76aa24 | ||
|
|
43d5c0f8ad | ||
|
|
4ec07ca053 | ||
|
|
1705d8993e | ||
|
|
309ba06f8a | ||
|
|
e65a1bf6e1 | ||
|
|
ff5b163c1d | ||
|
|
706bf4d3ba | ||
|
|
4d16d64cdc | ||
|
|
6df0a1d149 | ||
|
|
51111f707f | ||
|
|
214f2d9028 | ||
|
|
78ddd4d782 | ||
|
|
7d7eecaa5e | ||
|
|
4d7d3be93b | ||
|
|
ffc997b84e | ||
|
|
4b6a1c5902 | ||
|
|
0cf81040fb | ||
|
|
9fb6b7a880 | ||
|
|
a93f0a4452 | ||
|
|
26c6010ce0 | ||
|
|
3b16d783d3 | ||
|
|
940ddf0d00 | ||
|
|
3f4fb63029 | ||
|
|
61965f295d | ||
|
|
44d30ae583 | ||
|
|
6a168c884d | ||
|
|
dd553125d6 | ||
|
|
62d5490dc8 | ||
|
|
790571b6fc | ||
|
|
cc856d4d80 | ||
|
|
6184078c3f | ||
|
|
9c034c366b | ||
|
|
31ede51c42 | ||
|
|
0eff8a3e6a | ||
|
|
136b09d7f2 | ||
|
|
bdd8af1873 | ||
|
|
11b89b679d | ||
|
|
530a40b0aa | ||
|
|
4834dbf781 | ||
|
|
374bfb7a19 | ||
|
|
94931ef5a2 | ||
|
|
bc6453de2b | ||
|
|
533d398f9d | ||
|
|
d3a926b25a | ||
|
|
6b74db70bd | ||
|
|
282d3fcd99 | ||
|
|
72da718015 | ||
|
|
bf0e92d6e4 | ||
|
|
c53ddf1638 | ||
|
|
45f5843598 | ||
|
|
fc39588b2e | ||
|
|
635e7b7f59 | ||
|
|
13fcf265b7 | ||
|
|
8b43c81fc0 | ||
|
|
e624dd431b | ||
|
|
969abca95d | ||
|
|
9695e94608 | ||
|
|
b985362757 | ||
|
|
0d090fe9e2 | ||
|
|
95c4d51e67 | ||
|
|
c6987f6f67 | ||
|
|
5ad14205f3 | ||
|
|
02541184bd | ||
|
|
bced8bf566 | ||
|
|
faac421b63 | ||
|
|
22a6159520 | ||
|
|
f4e397ebb1 | ||
|
|
87de804a9e | ||
|
|
e3a4ed5363 | ||
|
|
3c688bfafa | ||
|
|
6e1ece8183 | ||
|
|
168341db39 | ||
|
|
d7e38d6393 | ||
|
|
cd0d7aa5a3 | ||
|
|
25558df22d | ||
|
|
679d98c8bf | ||
|
|
469ee29b5a | ||
|
|
96e535fdc4 | ||
|
|
4aab75b842 | ||
|
|
17a46ae642 | ||
|
|
d0b2820160 | ||
|
|
b7e5ced2e8 | ||
|
|
eaabe51537 | ||
|
|
83fa41bbd0 | ||
|
|
89be79192f | ||
|
|
c4df8eabf9 | ||
|
|
b575899d46 | ||
|
|
7bcb260712 | ||
|
|
1cb5a0789e | ||
|
|
8b7ef47aad | ||
|
|
e253a7b4ff | ||
|
|
fe5059dd51 | ||
|
|
25c6b311f5 | ||
|
|
665b33fa05 | ||
|
|
635c70cffd | ||
|
|
dc1c1db5e6 | ||
|
|
e2f7318922 | ||
|
|
c76200b0e3 | ||
|
|
62b4b63f4a | ||
|
|
574b85e177 | ||
|
|
e3e370d3ab | ||
|
|
1ac4de65a9 | ||
|
|
c19a58380c | ||
|
|
4bc50e4bad | ||
|
|
5ce3f84dfa | ||
|
|
b280201446 | ||
|
|
76c81395b7 | ||
|
|
7ff0e25711 | ||
|
|
cd46c69a66 | ||
|
|
b76871aa07 | ||
|
|
479194b589 | ||
|
|
3fe47f99a9 | ||
|
|
aeda38fa01 | ||
|
|
10aa34a8e8 | ||
|
|
e1135eac01 | ||
|
|
c805b6e0c5 | ||
|
|
3587352219 | ||
|
|
84e7ad43bc | ||
|
|
e833176c65 | ||
|
|
ef1fdc7f39 | ||
|
|
07e9101ac7 | ||
|
|
4d5cbcf475 | ||
|
|
d688f9a1c6 | ||
|
|
67ec8fbd91 | ||
|
|
aff166e27d | ||
|
|
f14d715070 | ||
|
|
7f0f423450 | ||
|
|
f067da31b4 | ||
|
|
fe3b8e6f88 | ||
|
|
46a27a3f3a | ||
|
|
82726520b8 | ||
|
|
d40937b467 | ||
|
|
f996b9414a | ||
|
|
146e0bf569 | ||
|
|
67a93016c0 | ||
|
|
14377e7348 | ||
|
|
8c4598e7a6 | ||
|
|
71be6aca1a | ||
|
|
ccf06956f9 | ||
|
|
1ba5c7f828 | ||
|
|
68011d6734 | ||
|
|
f7f723829c | ||
|
|
19a4b3c0ef | ||
|
|
3ef2902829 | ||
|
|
b861d3f668 | ||
|
|
e119c11204 | ||
|
|
b1998b61bc | ||
|
|
0fc49d312d | ||
|
|
c46a7b2486 | ||
|
|
1744527a41 | ||
|
|
074f6448e1 | ||
|
|
74168707c6 | ||
|
|
86b0d155fa | ||
|
|
416255f41a | ||
|
|
fa19af308d | ||
|
|
852161317d | ||
|
|
3285f829f6 | ||
|
|
812d10f374 | ||
|
|
cd289ffb1e | ||
|
|
89ff502964 | ||
|
|
b25d95eeb7 | ||
|
|
554f09c817 | ||
|
|
912011bfed | ||
|
|
78a1073e2a | ||
|
|
6daf6df2d0 | ||
|
|
1bf0d352a1 | ||
|
|
17d0213d58 | ||
|
|
f7c11febe5 | ||
|
|
991c855c11 | ||
|
|
b10a8d9479 | ||
|
|
0c132f6e43 | ||
|
|
4842f9cb0d | ||
|
|
04f8dc7d75 | ||
|
|
3da7b74cfb | ||
|
|
b89724ad59 | ||
|
|
a3920ec2d8 | ||
|
|
6a5e0243be | ||
|
|
91079eeb78 | ||
|
|
2278469a8b | ||
|
|
377cf8e6fc | ||
|
|
7d9a664d38 | ||
|
|
a6e1134dc3 | ||
|
|
6d6953d795 | ||
|
|
0c5eb77cba | ||
|
|
ba5975f29b | ||
|
|
2a21d7d0be | ||
|
|
945b761fc2 | ||
|
|
203d868aeb | ||
|
|
28b1c03e03 | ||
|
|
b085baf91b | ||
|
|
03f9284f30 | ||
|
|
7e7e98ff6b | ||
|
|
ba1dd1b50e | ||
|
|
895750ea14 | ||
|
|
c47f45ea46 | ||
|
|
f24b214229 | ||
|
|
5b32f83890 | ||
|
|
dbf7377c02 | ||
|
|
13b0fcf65f | ||
|
|
02763c9cb3 | ||
|
|
34fc625602 | ||
|
|
ed997d53e1 | ||
|
|
7c2d89e70f | ||
|
|
a77946ce2c | ||
|
|
36fb4587a1 | ||
|
|
013656bc4d | ||
|
|
5eaeb50000 | ||
|
|
2ee503946c | ||
|
|
be73b17d46 | ||
|
|
60674ec95c | ||
|
|
74ae57324b | ||
|
|
1e23a2fd67 | ||
|
|
5642e3283d | ||
|
|
6e1e4c4685 | ||
|
|
a9d4458103 | ||
|
|
17874c4658 | ||
|
|
ac4ca3e56e | ||
|
|
5cff346cb5 | ||
|
|
a0b2c1f3e2 | ||
|
|
ea84563bb4 | ||
|
|
3c67ee08a8 | ||
|
|
1c6244d235 | ||
|
|
9cd0079d9e | ||
|
|
d6a68ee3d9 | ||
|
|
8429cbfd6e | ||
|
|
14cb7f96fc | ||
|
|
ffca88845b | ||
|
|
c6722c4cbe | ||
|
|
b3a2cfabc6 | ||
|
|
018b228976 | ||
|
|
00a81ab4f0 | ||
|
|
e47b4d7959 | ||
|
|
4dfdb218e2 | ||
|
|
78cbb9e67d | ||
|
|
c78db71b14 | ||
|
|
3b492ead92 | ||
|
|
ab49322fec | ||
|
|
26cce3a46a | ||
|
|
0dac25d9e7 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_notes/
|
||||
17
.vscode/settings.json
vendored
Normal file
17
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Lua.diagnostics.globals": [
|
||||
"term",
|
||||
"fs",
|
||||
"peripheral",
|
||||
"rs",
|
||||
"bit",
|
||||
"parallel",
|
||||
"colors",
|
||||
"textutils",
|
||||
"shell",
|
||||
"settings",
|
||||
"window",
|
||||
"read",
|
||||
"periphemu"
|
||||
]
|
||||
}
|
||||
63
README.md
63
README.md
@@ -1,2 +1,61 @@
|
||||
# cc-mek-reactor-controller
|
||||
Configurable ComputerCraft multi-reactor control for Mekanism with a GUI, automatic safety features, waste processing control, and more!
|
||||
# cc-mek-scada
|
||||
Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fission reactors with a GUI, automatic safety features, waste processing control, and more!
|
||||
|
||||
This requires CC: Tweaked and Mekanism v10.0+ (10.1 recommended for full feature set).
|
||||
|
||||
|
||||
## [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.
|
||||
|
||||
This project implements concepts of a SCADA system in ComputerCraft (because why not? ..okay don't answer that). I recommend reviewing that linked wikipedia page on SCADA if you want to understand the concepts used here.
|
||||
|
||||

|
||||
|
||||
SCADA and industrial automation terminology is used throughout the project, such as:
|
||||
- Supervisory Computer: Gathers data and controls the process
|
||||
- Coordinating Computer: Used as the HMI component, user requests high-level processing operations
|
||||
- RTU: Remote Terminal Unit
|
||||
- PLC: Programmable Logic Controller
|
||||
|
||||
## ComputerCraft Architecture
|
||||
|
||||
### Coordinator Server
|
||||
|
||||
There can only be one of these. This server acts as a hybrid of levels 3 & 4 in the SCADA diagram above. In addition to viewing status and controlling processes with advanced monitors, it can host access for one or more Pocket computers.
|
||||
|
||||
### Supervisory Computers
|
||||
|
||||
There should be one of these per facility system. Currently, that means only one. In the future, multiple supervisors would provide the capability of coordinating between multiple facilities (like a fission facility, fusion facility, etc).
|
||||
|
||||
### RTUs
|
||||
|
||||
RTUs are effectively basic connections between a device and the SCADA system with no internal logic providing the system with I/O capabilities. A single Advanced Computer can represent multiple RTUs as instead I am modeling an RTU as the wired modems connected to that computer rather than the computer itself. Each RTU is referenced separately with an identifier in the modbus communications (see Communications section), so a single computer can distribute instructions to multiple devices. This should save on having a pile of computers everywhere (but if you want to have that, no one's stopping you).
|
||||
|
||||
The RTU control code is relatively unique, as instead of having instructions be decoded simply, due to using modbus, I implemented a generalized RTU interface. To fulfill this, each type of I/O operation is linked to a function rather than implementing the logic itself. For example, to connect an input register to a turbine `getFlowRate()` call, the function reference itself is passed to the `connect_input_reg()` function. A call to `read_input_reg()` on that register address will call the `turbine.getFlowRate()` function and return the result.
|
||||
|
||||
### PLCs
|
||||
|
||||
PLCs are advanced devices that allow for both reporting and control to/from the SCADA system in addition to programed behaviors independent of the SCADA system. Currently there is only one type of PLC, and that is the reactor PLC. This is responsible for reporting on and controlling the reactor as a part of the SCADA system, and independently regulating the safety of the reactor. It checks the status for multiple hazard scenarios and shuts down the reactor if any condition is satisfied.
|
||||
|
||||
There can and should only be one of these per reactor. A single Advanced Computer will act as the PLC, with either a direct connection (physical contact) or a wired modem connection to the reactor logic port.
|
||||
|
||||
## Communications
|
||||
|
||||
A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication protocol is used for communication with RTUs. Useful terminology for you to know:
|
||||
- Discrete Inputs: Single Bit Read-Only (digital inputs)
|
||||
- Coils: Single Bit Read/Write (digital I/O)
|
||||
- Input Registers: Multi-Byte Read-Only (analog inputs)
|
||||
- Holding Registers: Multi-Byte Read/Write (analog I/O)
|
||||
|
||||
### Security and Encryption
|
||||
|
||||
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.
|
||||
|
||||
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 only other possible security mitigation for commanding (no effect on monitoring) is to enforce a maximum authorized transmission range (which I will probably also do, or maybe fall back to), as modem message events contain the transmission distance.
|
||||
|
||||
## Known Issues
|
||||
|
||||
GitHub issue \#29:
|
||||
It appears that with Mekanism 10.0, a boiler peripheral may rapidly disconnect/reconnect constantly while running. This will prevent that RTU from operating correctly while also filling up the log file. This may be due to a very specific version interaction of CC: Tweaked and Mekansim, so you are welcome to try this on Mekanism 10.0 servers, but do be aware it may not work.
|
||||
|
||||
16
coordinator/apisessions.lua
Normal file
16
coordinator/apisessions.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
local apisessions = {}
|
||||
|
||||
---@param packet capi_frame
|
||||
function apisessions.handle_packet(packet)
|
||||
end
|
||||
|
||||
function apisessions.check_all_watchdogs()
|
||||
end
|
||||
|
||||
function apisessions.close_all()
|
||||
end
|
||||
|
||||
function apisessions.free_all_closed()
|
||||
end
|
||||
|
||||
return apisessions
|
||||
24
coordinator/config.lua
Normal file
24
coordinator/config.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
local config = {}
|
||||
|
||||
-- port of the SCADA supervisor
|
||||
config.SCADA_SV_PORT = 16100
|
||||
-- port to listen to incoming packets from supervisor
|
||||
config.SCADA_SV_LISTEN = 16101
|
||||
-- listen port for SCADA coordinator API access
|
||||
config.SCADA_API_LISTEN = 16200
|
||||
-- expected number of reactor units, used only to require that number of unit monitors
|
||||
config.NUM_UNITS = 4
|
||||
-- graphics color
|
||||
config.RECOLOR = true
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
-- crypto config
|
||||
config.SECURE = true
|
||||
-- must be common between all devices
|
||||
config.PASSWORD = "testpassword!"
|
||||
|
||||
return config
|
||||
459
coordinator/coordinator.lua
Normal file
459
coordinator/coordinator.lua
Normal file
@@ -0,0 +1,459 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local apisessions = require("coordinator.apisessions")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local dialog = require("coordinator.ui.dialog")
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
|
||||
|
||||
-- request the user to select a monitor
|
||||
---@param names table available monitors
|
||||
local function ask_monitor(names)
|
||||
println("available monitors:")
|
||||
for i = 1, #names do
|
||||
print(" " .. names[i])
|
||||
end
|
||||
println("")
|
||||
println("select a monitor or type c to cancel")
|
||||
|
||||
local iface = dialog.ask_options(names, "c")
|
||||
|
||||
if iface ~= false and iface ~= nil then
|
||||
util.filter_table(names, function (x) return x ~= iface end)
|
||||
end
|
||||
|
||||
return iface
|
||||
end
|
||||
|
||||
-- configure monitor layout
|
||||
---@param num_units integer number of units expected
|
||||
function coordinator.configure_monitors(num_units)
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
primary = nil,
|
||||
primary_name = "",
|
||||
unit_displays = {},
|
||||
unit_name_map = {}
|
||||
}
|
||||
|
||||
local monitors_avail = ppm.get_monitor_list()
|
||||
local names = {}
|
||||
|
||||
-- get all interface names
|
||||
for iface, _ in pairs(monitors_avail) do
|
||||
table.insert(names, iface)
|
||||
end
|
||||
|
||||
-- we need a certain number of monitors (1 per unit + 1 primary display)
|
||||
if #names < num_units + 1 then
|
||||
println("not enough monitors connected (need " .. num_units + 1 .. ")")
|
||||
log.warning("insufficient monitors present (need " .. num_units + 1 .. ")")
|
||||
return false
|
||||
end
|
||||
|
||||
-- attempt to load settings
|
||||
settings.load("/coord.settings")
|
||||
|
||||
---------------------
|
||||
-- PRIMARY DISPLAY --
|
||||
---------------------
|
||||
|
||||
local iface_primary_display = settings.get("PRIMARY_DISPLAY")
|
||||
|
||||
if not util.table_contains(names, iface_primary_display) then
|
||||
println("primary display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
iface_primary_display = nil
|
||||
end
|
||||
|
||||
while iface_primary_display == nil and #names > 0 do
|
||||
-- lets get a monitor
|
||||
iface_primary_display = ask_monitor(names)
|
||||
end
|
||||
|
||||
if iface_primary_display == false then return false end
|
||||
|
||||
settings.set("PRIMARY_DISPLAY", iface_primary_display)
|
||||
util.filter_table(names, function (x) return x ~= iface_primary_display end)
|
||||
|
||||
monitors.primary = ppm.get_periph(iface_primary_display)
|
||||
monitors.primary_name = iface_primary_display
|
||||
|
||||
-------------------
|
||||
-- UNIT DISPLAYS --
|
||||
-------------------
|
||||
|
||||
local unit_displays = settings.get("UNIT_DISPLAYS")
|
||||
|
||||
if unit_displays == nil then
|
||||
unit_displays = {}
|
||||
for i = 1, num_units do
|
||||
local display = nil
|
||||
|
||||
while display == nil and #names > 0 do
|
||||
-- lets get a monitor
|
||||
println("please select monitor for unit " .. i)
|
||||
display = ask_monitor(names)
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
else
|
||||
-- make sure all displays are connected
|
||||
for i = 1, num_units do
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local display = unit_displays[i]
|
||||
|
||||
if not util.table_contains(names, display) then
|
||||
local response = dialog.ask_y_n("unit display " .. i .. " is not connected, would you like to change it?", true)
|
||||
if response == false then return false end
|
||||
display = nil
|
||||
end
|
||||
|
||||
while display == nil and #names > 0 do
|
||||
-- lets get a monitor
|
||||
display = ask_monitor(names)
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
end
|
||||
|
||||
settings.set("UNIT_DISPLAYS", unit_displays)
|
||||
settings.save("/coord.settings")
|
||||
|
||||
for i = 1, #unit_displays do
|
||||
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
|
||||
monitors.unit_name_map[i] = unit_displays[i]
|
||||
end
|
||||
|
||||
return true, monitors
|
||||
end
|
||||
|
||||
-- dmesg print wrapper
|
||||
---@param message string message
|
||||
---@param dmesg_tag string tag
|
||||
---@param working? boolean to use dmesg_working
|
||||
---@return function? update, function? done
|
||||
local function log_dmesg(message, dmesg_tag, working)
|
||||
local colors = {
|
||||
GRAPHICS = colors.green,
|
||||
SYSTEM = colors.cyan,
|
||||
BOOT = colors.blue,
|
||||
COMMS = colors.purple
|
||||
}
|
||||
|
||||
if working then
|
||||
return log.dmesg_working(message, dmesg_tag, colors[dmesg_tag])
|
||||
else
|
||||
log.dmesg(message, dmesg_tag, colors[dmesg_tag])
|
||||
end
|
||||
end
|
||||
|
||||
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
|
||||
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
|
||||
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
|
||||
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
|
||||
|
||||
---@param message string
|
||||
---@return function update, function done
|
||||
function coordinator.log_comms_connecting(message) return log_dmesg(message, "COMMS", true) end
|
||||
|
||||
-- coordinator communications
|
||||
---@param version string
|
||||
---@param modem table
|
||||
---@param sv_port integer
|
||||
---@param sv_listen integer
|
||||
---@param api_listen integer
|
||||
---@param sv_watchdog watchdog
|
||||
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_seq_num = 0,
|
||||
sv_r_seq_num = nil,
|
||||
modem = modem,
|
||||
connected = false
|
||||
}
|
||||
|
||||
---@class coord_comms
|
||||
local public = {}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- open all channels
|
||||
local function _open_channels()
|
||||
if not self.modem.isOpen(sv_listen) then
|
||||
self.modem.open(sv_listen)
|
||||
end
|
||||
|
||||
if not self.modem.isOpen(api_listen) then
|
||||
self.modem.open(api_listen)
|
||||
end
|
||||
end
|
||||
|
||||
-- open at construct time
|
||||
_open_channels()
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = nil ---@type mgmt_packet|crdn_packet
|
||||
|
||||
if protocol == PROTOCOLS.SCADA_MGMT then
|
||||
pkt = comms.mgmt_packet()
|
||||
elseif protocol == PROTOCOLS.SCADA_CRDN then
|
||||
pkt = comms.crdn_packet()
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.ESTABLISH, { version })
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- reconnect a newly connected modem
|
||||
---@param modem table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_modem(modem)
|
||||
self.modem = modem
|
||||
_open_channels()
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
self.sv_linked = false
|
||||
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {})
|
||||
end
|
||||
|
||||
-- attempt to connect to the subervisor
|
||||
---@param timeout_s number timeout in seconds
|
||||
---@param tick_dmesg_waiting function callback to tick dmesg waiting
|
||||
---@param task_done function callback to show done on dmesg
|
||||
---@return boolean sv_linked true if connected, false otherwise
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
|
||||
local clock = util.new_clock(1)
|
||||
local start = util.time_s()
|
||||
local terminated = false
|
||||
|
||||
_send_establish()
|
||||
|
||||
clock.start()
|
||||
|
||||
while (util.time_s() - start) < timeout_s and not self.sv_linked do
|
||||
local event, p1, p2, p3, p4, p5 = util.pull_event()
|
||||
|
||||
if event == "timer" and clock.is_clock(p1) then
|
||||
-- timed out attempt, try again
|
||||
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
|
||||
_send_establish()
|
||||
clock.start()
|
||||
elseif event == "modem_message" then
|
||||
-- handle message
|
||||
local packet = public.parse_packet(p1, p2, p3, p4, p5)
|
||||
if packet ~= nil and packet.type == SCADA_CRDN_TYPES.ESTABLISH then
|
||||
public.handle_packet(packet)
|
||||
end
|
||||
elseif event == "terminate" then
|
||||
terminated = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
task_done(self.sv_linked)
|
||||
|
||||
if terminated then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
end
|
||||
|
||||
return self.sv_linked
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|crdn_frame|capi_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local s_pkt = comms.scada_packet()
|
||||
|
||||
-- parse packet as generic SCADA packet
|
||||
s_pkt.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as SCADA management packet
|
||||
if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
-- get as coordinator packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
-- get as coordinator API packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.COORD_API then
|
||||
local capi_pkt = comms.capi_packet()
|
||||
if capi_pkt.decode(s_pkt) then
|
||||
pkt = capi_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|capi_frame
|
||||
function public.handle_packet(packet)
|
||||
if packet ~= nil then
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if protocol == PROTOCOLS.COORD_API then
|
||||
apisessions.handle_packet(packet)
|
||||
else
|
||||
-- check sequence number
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.connected and self.sv_r_seq_num >= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
sv_watchdog.feed()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOLS.SCADA_CRDN then
|
||||
if packet.type == SCADA_CRDN_TYPES.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
if packet.length > 1 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = packet.data[1],
|
||||
defs = {} -- boilers and turbines
|
||||
}
|
||||
|
||||
if (packet.length - 1) == (conf.num_units * 2) then
|
||||
-- record sequence of pairs of [#boilers, #turbines] per unit
|
||||
for i = 2, packet.length do
|
||||
table.insert(conf.defs, packet.data[i])
|
||||
end
|
||||
|
||||
-- init io controller
|
||||
iocontrol.init(conf)
|
||||
|
||||
self.sv_linked = true
|
||||
else
|
||||
log.debug("supervisor conn establish packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("supervisor conn establish packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.STRUCT_BUILDS then
|
||||
-- record builds
|
||||
if iocontrol.record_builds(packet.data) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.STRUCT_BUILDS, {})
|
||||
else
|
||||
log.error("received invalid build packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_statuses(packet.data) then
|
||||
log.error("received invalid unit statuses packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.COMMAND_UNIT then
|
||||
elseif packet.type == SCADA_CRDN_TYPES.ALARM then
|
||||
else
|
||||
log.warning("received unknown SCADA_CRDN packet type " .. packet.type)
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 500 then
|
||||
log.warning("coord KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("coord RTT = " .. trip_time .. "ms")
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_linked = false
|
||||
println_ts("server connection closed by remote host")
|
||||
log.warning("server connection closed by remote host")
|
||||
else
|
||||
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
else
|
||||
-- should be unreachable assuming packet is from parse_packet()
|
||||
log.error("illegal packet type " .. protocol, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the coordinator is still linked to the supervisor
|
||||
function public.is_linked() return self.sv_linked end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return coordinator
|
||||
285
coordinator/iocontrol.lua
Normal file
285
coordinator/iocontrol.lua
Normal file
@@ -0,0 +1,285 @@
|
||||
local psil = require("scada-common.psil")
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local iocontrol = {}
|
||||
|
||||
---@class ioctl
|
||||
local io = {}
|
||||
|
||||
-- initialize the coordinator IO controller
|
||||
---@param conf facility_conf configuration
|
||||
function iocontrol.init(conf)
|
||||
io.facility = {
|
||||
scram = false,
|
||||
num_units = conf.num_units,
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
io.units = {}
|
||||
for i = 1, conf.num_units do
|
||||
---@class ioctl_entry
|
||||
local entry = {
|
||||
unit_id = i, ---@type integer
|
||||
initialized = false,
|
||||
|
||||
num_boilers = 0,
|
||||
num_turbines = 0,
|
||||
|
||||
control_state = false,
|
||||
burn_rate_cmd = 0.0,
|
||||
waste_control = 0,
|
||||
|
||||
---@fixme debug stubs to be linked into comms later?
|
||||
start = function () print("UNIT " .. i .. ": start") end,
|
||||
scram = function () print("UNIT " .. i .. ": SCRAM") end,
|
||||
set_burn = function (rate) print("UNIT " .. i .. ": set burn rate to " .. rate) end,
|
||||
|
||||
reactor_ps = psil.create(),
|
||||
reactor_data = {}, ---@type reactor_db
|
||||
|
||||
boiler_ps_tbl = {},
|
||||
boiler_data_tbl = {},
|
||||
|
||||
turbine_ps_tbl = {},
|
||||
turbine_data_tbl = {}
|
||||
}
|
||||
|
||||
for _ = 1, conf.defs[(i * 2) - 1] do
|
||||
local data = {} ---@type boiler_session_db|boilerv_session_db
|
||||
table.insert(entry.boiler_ps_tbl, psil.create())
|
||||
table.insert(entry.boiler_data_tbl, data)
|
||||
end
|
||||
|
||||
for _ = 1, conf.defs[i * 2] do
|
||||
local data = {} ---@type turbine_session_db|turbinev_session_db
|
||||
table.insert(entry.turbine_ps_tbl, psil.create())
|
||||
table.insert(entry.turbine_data_tbl, data)
|
||||
end
|
||||
|
||||
entry.num_boilers = #entry.boiler_data_tbl
|
||||
entry.num_turbines = #entry.turbine_data_tbl
|
||||
|
||||
table.insert(io.units, entry)
|
||||
end
|
||||
end
|
||||
|
||||
-- populate structure builds
|
||||
---@param builds table
|
||||
---@return boolean valid
|
||||
function iocontrol.record_builds(builds)
|
||||
if #builds ~= #io.units then
|
||||
log.error("number of provided unit builds does not match expected number of units")
|
||||
return false
|
||||
else
|
||||
for i = 1, #builds do
|
||||
local unit = io.units[i] ---@type ioctl_entry
|
||||
local build = builds[i]
|
||||
|
||||
-- reactor build
|
||||
unit.reactor_data.mek_struct = build.reactor
|
||||
for key, val in pairs(unit.reactor_data.mek_struct) do
|
||||
unit.reactor_ps.publish(key, val)
|
||||
end
|
||||
|
||||
-- boiler builds
|
||||
for id, boiler in pairs(build.boilers) do
|
||||
unit.boiler_data_tbl[id] = {
|
||||
formed = boiler[2], ---@type boolean|nil
|
||||
build = boiler[1] ---@type table
|
||||
}
|
||||
|
||||
unit.boiler_ps_tbl[id].publish("formed", boiler[2])
|
||||
|
||||
for key, val in pairs(unit.boiler_data_tbl[id].build) do
|
||||
unit.boiler_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
-- turbine builds
|
||||
for id, turbine in pairs(build.turbines) do
|
||||
unit.turbine_data_tbl[id] = {
|
||||
formed = turbine[2], ---@type boolean|nil
|
||||
build = turbine[1] ---@type table
|
||||
}
|
||||
|
||||
unit.turbine_ps_tbl[id].publish("formed", turbine[2])
|
||||
|
||||
for key, val in pairs(unit.turbine_data_tbl[id].build) do
|
||||
unit.turbine_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- update unit statuses
|
||||
---@param statuses table
|
||||
---@return boolean valid
|
||||
function iocontrol.update_statuses(statuses)
|
||||
if #statuses ~= #io.units then
|
||||
log.error("number of provided unit statuses does not match expected number of units")
|
||||
return false
|
||||
else
|
||||
for i = 1, #statuses do
|
||||
local unit = io.units[i] ---@type ioctl_entry
|
||||
local status = statuses[i]
|
||||
|
||||
-- reactor PLC status
|
||||
|
||||
local reactor_status = status[1]
|
||||
|
||||
if #reactor_status == 0 then
|
||||
unit.reactor_ps.publish("computed_status", 1) -- disconnected
|
||||
else
|
||||
local mek_status = reactor_status[1]
|
||||
local rps_status = reactor_status[2]
|
||||
local gen_status = reactor_status[3]
|
||||
|
||||
unit.reactor_data.last_status_update = gen_status[1]
|
||||
unit.reactor_data.control_state = gen_status[2]
|
||||
unit.reactor_data.rps_tripped = gen_status[3]
|
||||
unit.reactor_data.rps_trip_cause = gen_status[4]
|
||||
unit.reactor_data.degraded = gen_status[5]
|
||||
|
||||
unit.reactor_data.rps_status = rps_status ---@type rps_status
|
||||
unit.reactor_data.mek_status = mek_status ---@type mek_status
|
||||
|
||||
if unit.reactor_data.mek_status.status then
|
||||
unit.reactor_ps.publish("computed_status", 3) -- running
|
||||
else
|
||||
if unit.reactor_data.degraded then
|
||||
unit.reactor_ps.publish("computed_status", 5) -- faulted
|
||||
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
|
||||
unit.reactor_ps.publish("computed_status", 4) -- SCRAM
|
||||
else
|
||||
unit.reactor_ps.publish("computed_status", 2) -- disabled
|
||||
end
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data) do
|
||||
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
|
||||
unit.reactor_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.rps_status) do
|
||||
unit.reactor_ps.publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.mek_status) do
|
||||
unit.reactor_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
-- annunciator
|
||||
|
||||
local annunciator = status[2] ---@type annunciator
|
||||
|
||||
for key, val in pairs(annunciator) do
|
||||
if key == "TurbineTrip" then
|
||||
-- split up turbine trip table for all turbines and a general OR combination
|
||||
local trips = val
|
||||
local any = false
|
||||
|
||||
for id = 1, #trips do
|
||||
any = any or trips[id]
|
||||
unit.turbine_ps_tbl[id].publish(key, trips[id])
|
||||
end
|
||||
|
||||
unit.reactor_ps.publish("TurbineTrip", any)
|
||||
elseif key == "BoilerOnline" or key == "HeatingRateLow" then
|
||||
-- split up array for all boilers
|
||||
for id = 1, #val do
|
||||
unit.boiler_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
|
||||
-- split up array for all turbines
|
||||
for id = 1, #val do
|
||||
unit.turbine_ps_tbl[id].publish(key, val[id])
|
||||
end
|
||||
elseif type(val) == "table" then
|
||||
-- we missed one of the tables?
|
||||
log.error("unrecognized table found in annunciator list, this is a bug", true)
|
||||
else
|
||||
-- non-table fields
|
||||
unit.reactor_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
-- RTU statuses
|
||||
|
||||
local rtu_statuses = status[3]
|
||||
|
||||
-- boiler statuses
|
||||
|
||||
for id = 1, #unit.boiler_data_tbl do
|
||||
if rtu_statuses.boilers[i] == nil then
|
||||
-- disconnected
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 1)
|
||||
end
|
||||
end
|
||||
|
||||
for id, boiler in pairs(rtu_statuses.boilers) do
|
||||
unit.boiler_data_tbl[id].state = boiler[1] ---@type table
|
||||
unit.boiler_data_tbl[id].tanks = boiler[2] ---@type table
|
||||
|
||||
local data = unit.boiler_data_tbl[id] ---@type boiler_session_db|boilerv_session_db
|
||||
|
||||
if data.state.boil_rate > 0 then
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- active
|
||||
else
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- idle
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.boiler_data_tbl[id].state) do
|
||||
unit.boiler_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
|
||||
unit.boiler_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
-- turbine statuses
|
||||
|
||||
for id = 1, #unit.turbine_ps_tbl do
|
||||
if rtu_statuses.turbines[i] == nil then
|
||||
-- disconnected
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 1)
|
||||
end
|
||||
end
|
||||
|
||||
for id, turbine in pairs(rtu_statuses.turbines) do
|
||||
unit.turbine_data_tbl[id].state = turbine[1] ---@type table
|
||||
unit.turbine_data_tbl[id].tanks = turbine[2] ---@type table
|
||||
|
||||
local data = unit.turbine_data_tbl[id] ---@type turbine_session_db|turbinev_session_db
|
||||
|
||||
if data.tanks.steam_fill >= 0.99 then
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- trip
|
||||
elseif data.state.flow_rate < 100 then
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- idle
|
||||
else
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- active
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.turbine_data_tbl[id].state) do
|
||||
unit.turbine_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
|
||||
unit.turbine_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- get the IO controller database
|
||||
function iocontrol.get_db() return io end
|
||||
|
||||
return iocontrol
|
||||
157
coordinator/renderer.lua
Normal file
157
coordinator/renderer.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
local renderer = {}
|
||||
|
||||
-- render engine
|
||||
local engine = {
|
||||
monitors = nil,
|
||||
dmesg_window = nil,
|
||||
ui_ready = false
|
||||
}
|
||||
|
||||
-- UI layouts
|
||||
local ui = {
|
||||
main_layout = nil,
|
||||
unit_layouts = {}
|
||||
}
|
||||
|
||||
-- reset a display to the "default", but set text scale to 0.5
|
||||
---@param monitor table monitor
|
||||
---@param recolor? boolean override default color palette
|
||||
local function _reset_display(monitor, recolor)
|
||||
monitor.setTextScale(0.5)
|
||||
monitor.setTextColor(colors.white)
|
||||
monitor.setBackgroundColor(colors.black)
|
||||
monitor.clear()
|
||||
monitor.setCursorPos(1, 1)
|
||||
|
||||
if recolor then
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
monitor.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
else
|
||||
-- reset all colors
|
||||
for _, val in pairs(colors) do
|
||||
-- colors api has constants and functions, just get color constants
|
||||
if type(val) == "number" then
|
||||
monitor.setPaletteColor(val, term.nativePaletteColor(val))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- link to the monitor peripherals
|
||||
---@param monitors monitors_struct
|
||||
function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
end
|
||||
|
||||
-- check if the renderer is configured to use a given monitor peripheral
|
||||
---@param periph table peripheral
|
||||
---@return boolean is_used
|
||||
function renderer.is_monitor_used(periph)
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == periph then
|
||||
return true
|
||||
else
|
||||
for i = 1, #engine.monitors.unit_displays do
|
||||
if engine.monitors.unit_displays[i] == periph then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- reset all displays in use by the renderer
|
||||
---@param recolor? boolean true to use color palette from style
|
||||
function renderer.reset(recolor)
|
||||
-- reset primary monitor
|
||||
_reset_display(engine.monitors.primary, recolor)
|
||||
|
||||
-- reset unit displays
|
||||
for _, monitor in pairs(engine.monitors.unit_displays) do
|
||||
_reset_display(monitor, recolor)
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize the dmesg output window
|
||||
function renderer.init_dmesg()
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y)
|
||||
|
||||
log.direct_dmesg(engine.dmesg_window)
|
||||
end
|
||||
|
||||
-- start the coordinator GUI
|
||||
function renderer.start_ui()
|
||||
if not engine.ui_ready then
|
||||
-- hide dmesg
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
-- show main view on main monitor
|
||||
ui.main_layout = main_view(engine.monitors.primary)
|
||||
|
||||
-- show unit views on unit displays
|
||||
for id, monitor in pairs(engine.monitors.unit_displays) do
|
||||
table.insert(ui.unit_layouts, unit_view(monitor, id))
|
||||
end
|
||||
|
||||
-- report ui as ready
|
||||
engine.ui_ready = true
|
||||
end
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
if engine.ui_ready then
|
||||
-- report ui as not ready
|
||||
engine.ui_ready = false
|
||||
|
||||
-- hide to stop animation callbacks
|
||||
ui.main_layout.hide()
|
||||
for i = 1, #ui.unit_layouts do
|
||||
ui.unit_layouts[i].hide()
|
||||
engine.monitors.unit_displays[i].clear()
|
||||
end
|
||||
|
||||
-- clear root UI elements
|
||||
ui.main_layout = nil
|
||||
ui.unit_layouts = {}
|
||||
|
||||
-- re-draw dmesg
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- is the UI ready?
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a touch event
|
||||
---@param event monitor_touch
|
||||
function renderer.handle_touch(event)
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
ui.main_layout.handle_touch(event)
|
||||
else
|
||||
for id, monitor in pairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = ui.unit_layouts[id] ---@type graphics_element
|
||||
layout.handle_touch(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return renderer
|
||||
318
coordinator/startup.lua
Normal file
318
coordinator/startup.lua
Normal file
@@ -0,0 +1,318 @@
|
||||
--
|
||||
-- Nuclear Generation Facility SCADA Coordinator
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcallbackdsp = require("scada-common.tcallbackdsp")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local apisessions = require("coordinator.apisessions")
|
||||
local config = require("coordinator.config")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local renderer = require("coordinator.renderer")
|
||||
|
||||
local COORDINATOR_VERSION = "alpha-v0.4.12"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local log_graphics = coordinator.log_graphics
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_comms_connecting = coordinator.log_comms_connecting
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_port(config.SCADA_SV_PORT)
|
||||
cfv.assert_port(config.SCADA_SV_LISTEN)
|
||||
cfv.assert_port(config.SCADA_API_LISTEN)
|
||||
cfv.assert_type_int(config.NUM_UNITS)
|
||||
cfv.assert_type_bool(config.RECOLOR)
|
||||
cfv.assert_type_str(config.LOG_PATH)
|
||||
cfv.assert_type_int(config.LOG_MODE)
|
||||
cfv.assert_type_bool(config.SECURE)
|
||||
cfv.assert_type_str(config.PASSWORD)
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||
log.info("========================================")
|
||||
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
|
||||
|
||||
----------------------------------------
|
||||
-- system startup
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
-- setup monitors
|
||||
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
|
||||
if not configured then
|
||||
println("boot> monitor setup failed")
|
||||
log.fatal("monitor configuration failed")
|
||||
return
|
||||
end
|
||||
|
||||
log.info("monitors ready, dmesg output incoming...")
|
||||
|
||||
-- init renderer
|
||||
renderer.set_displays(monitors)
|
||||
renderer.reset(config.RECOLOR)
|
||||
renderer.init_dmesg()
|
||||
|
||||
log_graphics("displays connected and reset")
|
||||
log_sys("system start on " .. os.date("%c"))
|
||||
log_boot("starting " .. COORDINATOR_VERSION)
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications
|
||||
----------------------------------------
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
log_comms("wireless modem not found")
|
||||
println("boot> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
return
|
||||
else
|
||||
log_comms("wireless modem connected")
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(5)
|
||||
conn_watchdog.cancel()
|
||||
log.debug("boot> conn watchdog created")
|
||||
|
||||
-- start comms, open all channels
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, config.SCADA_API_LISTEN, conn_watchdog)
|
||||
log.debug("boot> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
-- base loop clock (2Hz, 10 ticks)
|
||||
local MAIN_CLOCK = 0.5
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
----------------------------------------
|
||||
-- connect to the supervisor
|
||||
----------------------------------------
|
||||
|
||||
-- attempt to connect to the supervisor or exit
|
||||
local function init_connect_sv()
|
||||
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT)
|
||||
|
||||
-- attempt to establish a connection with the supervisory computer
|
||||
if not coord_comms.sv_connect(60, tick_waiting, task_done) then
|
||||
log_comms("supervisor connection failed")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if not init_connect_sv() then
|
||||
println("boot> failed to connect to supervisor")
|
||||
log_sys("system shutdown")
|
||||
return
|
||||
else
|
||||
log_sys("supervisor connected, proceeding to UI start")
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
----------------------------------------
|
||||
|
||||
-- start up the UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function init_start_ui()
|
||||
log_graphics("starting UI...")
|
||||
-- util.psleep(3)
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, message = pcall(renderer.start_ui)
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
log_graphics(util.c("UI crashed: ", message))
|
||||
println_ts("UI crashed")
|
||||
log.fatal(util.c("ui crashed with error ", message))
|
||||
else
|
||||
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
|
||||
local ui_ok = init_start_ui()
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
----------------------------------------
|
||||
|
||||
local no_modem = false
|
||||
|
||||
-- start connection watchdog
|
||||
conn_watchdog.feed()
|
||||
log.debug("boot> conn watchdog started")
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- event loop
|
||||
-- ui_ok will never change in this loop, same as while true or exit if UI start failed
|
||||
while ui_ok do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
if device == modem then
|
||||
no_modem = true
|
||||
log_sys("comms modem disconnected")
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.error("comms modem disconnected!")
|
||||
|
||||
-- close out UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.is_monitor_used(device) then
|
||||
-- "halt and catch fire" style handling
|
||||
log_sys("lost a configured monitor, system will now exit")
|
||||
break
|
||||
else
|
||||
log_sys("lost unused monitor, ignoring")
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() then
|
||||
-- reconnected modem
|
||||
no_modem = false
|
||||
modem = device
|
||||
coord_comms.reconnect_modem(modem)
|
||||
|
||||
log_sys("comms modem reconnected")
|
||||
println_ts("wireless modem reconnected.")
|
||||
|
||||
-- re-init system
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
-- not supported, system will exit on loss of in-use monitors
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- free any closed sessions
|
||||
--apisessions.free_all_closed()
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
local msg = "supervisor server timeout"
|
||||
log_comms(msg)
|
||||
println_ts(msg)
|
||||
log.warning(msg)
|
||||
|
||||
-- close connection and UI
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
|
||||
if not no_modem then
|
||||
-- try to re-connect to the supervisor
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
end
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
--check API watchdogs
|
||||
--apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcallbackdsp.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
coord_comms.handle_packet(packet)
|
||||
|
||||
-- check if it was a disconnect
|
||||
if not coord_comms.is_linked() then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection and UI
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
|
||||
if not no_modem then
|
||||
-- try to re-connect to the supervisor
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_touch" then
|
||||
-- handle a monitor touch event
|
||||
renderer.handle_touch(core.events.touch(param1, param2, param3))
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
println_ts("terminate requested, closing connections...")
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
renderer.close_ui()
|
||||
log_sys("system shutdown")
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
49
coordinator/ui/components/boiler.lua
Normal file
49
coordinator/ui/components/boiler.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
|
||||
-- new boiler view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
local status = StateIndicator{parent=boiler,x=10,y=1,states=style.boiler.states,value=1,min_width=10}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||
|
||||
ps.subscribe("computed_status", status.update)
|
||||
ps.subscribe("temperature", temp.update)
|
||||
ps.subscribe("boil_rate", boil_r.update)
|
||||
|
||||
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="S",x=27,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=boiler,text="C",x=28,y=5,height=1,width=1,fg_bg=text_fg_bg}
|
||||
|
||||
local hcool = VerticalBar{parent=boiler,x=2,y=1,fg_bg=cpair(colors.orange,colors.gray),height=4,width=1}
|
||||
local water = VerticalBar{parent=boiler,x=3,y=1,fg_bg=cpair(colors.blue,colors.gray),height=4,width=1}
|
||||
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
|
||||
|
||||
ps.subscribe("hcool_fill", hcool.update)
|
||||
ps.subscribe("water_fill", water.update)
|
||||
ps.subscribe("steam_fill", steam.update)
|
||||
ps.subscribe("ccool_fill", ccool.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
63
coordinator/ui/components/reactor.lua
Normal file
63
coordinator/ui/components/reactor.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
|
||||
-- create new reactor view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param data reactor_db reactor data
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, data, ps)
|
||||
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
local status = StateIndicator{parent=reactor,x=8,y=1,states=style.reactor.states,value=1,min_width=14}
|
||||
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.1f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
|
||||
|
||||
ps.subscribe("computed_status", status.update)
|
||||
ps.subscribe("temp", core_temp.update)
|
||||
ps.subscribe("act_burn_rate", burn_r.update)
|
||||
ps.subscribe("heating_rate", heating_r.update)
|
||||
|
||||
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
|
||||
|
||||
TextBox{parent=reactor_fills,text="FUEL",x=2,y=1,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="COOL",x=2,y=2,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="HCOOL",x=2,y=4,height=1,fg_bg=text_fg_bg}
|
||||
TextBox{parent=reactor_fills,text="WASTE",x=2,y=5,height=1,fg_bg=text_fg_bg}
|
||||
|
||||
-- local ccool_color = util.trinary(data.mek_status.ccool_type == "sodium", cpair(colors.lightBlue,colors.gray), cpair(colors.blue,colors.gray))
|
||||
-- local hcool_color = util.trinary(data.mek_status.hcool_type == "superheated_sodium", cpair(colors.orange,colors.gray), cpair(colors.white,colors.gray))
|
||||
local ccool_color = util.trinary(true, cpair(colors.lightBlue,colors.gray), cpair(colors.blue,colors.gray))
|
||||
local hcool_color = util.trinary(true, cpair(colors.orange,colors.gray), cpair(colors.white,colors.gray))
|
||||
|
||||
local fuel = HorizontalBar{parent=reactor_fills,x=8,y=1,show_percent=true,bar_fg_bg=cpair(colors.black,colors.gray),height=1,width=14}
|
||||
local ccool = HorizontalBar{parent=reactor_fills,x=8,y=2,show_percent=true,bar_fg_bg=ccool_color,height=1,width=14}
|
||||
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=hcool_color,height=1,width=14}
|
||||
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
|
||||
|
||||
ps.subscribe("fuel_fill", fuel.update)
|
||||
ps.subscribe("ccool_fill", ccool.update)
|
||||
ps.subscribe("hcool_fill", hcool.update)
|
||||
ps.subscribe("waste_fill", waste.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
37
coordinator/ui/components/turbine.lua
Normal file
37
coordinator/ui/components/turbine.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
|
||||
-- new turbine view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
local status = StateIndicator{parent=turbine,x=8,y=1,states=style.turbine.states,value=1,min_width=10}
|
||||
local prod_rate = DataIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit="MFE",format="%10.2f",value=0,width=16,fg_bg=text_fg_bg}
|
||||
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
|
||||
|
||||
ps.subscribe("computed_status", status.update)
|
||||
ps.subscribe("prod_rate", prod_rate.update)
|
||||
ps.subscribe("flow_rate", flow_rate.update)
|
||||
|
||||
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=5,width=2}
|
||||
|
||||
ps.subscribe("steam_fill", steam.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
289
coordinator/ui/components/unit_detail.lua
Normal file
289
coordinator/ui/components/unit_detail.lua
Normal file
@@ -0,0 +1,289 @@
|
||||
--
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local tcallbackdsp = require("scada-common.tcallbackdsp")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local ColorMap = require("graphics.elements.colormap")
|
||||
|
||||
local CoreMap = require("graphics.elements.indicators.coremap")
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
|
||||
local MultiButton = require("graphics.elements.controls.multi_button")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local SCRAMButton = require("graphics.elements.controls.scram_button")
|
||||
local StartButton = require("graphics.elements.controls.start_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer
|
||||
local function init(parent, id)
|
||||
local unit = iocontrol.get_db().units[id] ---@type ioctl_entry
|
||||
local r_ps = unit.reactor_ps
|
||||
local b_ps = unit.boiler_ps_tbl
|
||||
local t_ps = unit.turbine_ps_tbl
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local scram_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
|
||||
-- main stats and core map --
|
||||
|
||||
---@todo need to be checking actual reactor dimensions somehow
|
||||
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
|
||||
r_ps.subscribe("temp", core_map.update)
|
||||
|
||||
local stat_fg_bg = cpair(colors.black,colors.white)
|
||||
|
||||
TextBox{parent=main,x=21,y=3,text="Core Temp",height=1,fg_bg=style.label}
|
||||
local core_temp = DataIndicator{parent=main,x=21,label="",format="%9.2f",value=0,unit="K",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
|
||||
r_ps.subscribe("temp", core_temp.update)
|
||||
main.line_break()
|
||||
|
||||
TextBox{parent=main,x=21,text="Burn Rate",height=1,width=12,fg_bg=style.label}
|
||||
local act_burn_r = DataIndicator{parent=main,x=21,label="",format="%6.1f",value=0,unit="mB/t",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
|
||||
r_ps.subscribe("act_burn_rate", act_burn_r.update)
|
||||
main.line_break()
|
||||
|
||||
TextBox{parent=main,x=21,text="Commanded Burn Rate",height=2,width=12,fg_bg=style.label}
|
||||
local burn_r = DataIndicator{parent=main,x=21,label="",format="%6.1f",value=0,unit="mB/t",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
|
||||
r_ps.subscribe("burn_rate", burn_r.update)
|
||||
main.line_break()
|
||||
|
||||
TextBox{parent=main,x=21,text="Heating Rate",height=1,width=12,fg_bg=style.label}
|
||||
local heating_r = DataIndicator{parent=main,x=21,label="",format="%11.0f",value=0,unit="",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
|
||||
r_ps.subscribe("heating_rate", heating_r.update)
|
||||
main.line_break()
|
||||
|
||||
TextBox{parent=main,x=21,text="Containment Integrity",height=2,width=12,fg_bg=style.label}
|
||||
local integ = DataIndicator{parent=main,x=21,label="",format="%9.0f",value=100,unit="%",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg}
|
||||
r_ps.subscribe("damage", function (x) integ.update(100.0 - x) end)
|
||||
main.line_break()
|
||||
|
||||
-- TextBox{parent=main,text="FL",x=21,y=19,height=1,width=2,fg_bg=style.label}
|
||||
-- TextBox{parent=main,text="WS",x=24,y=19,height=1,width=2,fg_bg=style.label}
|
||||
-- TextBox{parent=main,text="CL",x=28,y=19,height=1,width=2,fg_bg=style.label}
|
||||
-- TextBox{parent=main,text="HC",x=31,y=19,height=1,width=2,fg_bg=style.label}
|
||||
|
||||
-- local fuel = VerticalBar{parent=main,x=21,y=12,fg_bg=cpair(colors.black,colors.gray),height=6,width=2}
|
||||
-- local waste = VerticalBar{parent=main,x=24,y=12,fg_bg=cpair(colors.brown,colors.gray),height=6,width=2}
|
||||
-- local ccool = VerticalBar{parent=main,x=28,y=12,fg_bg=cpair(colors.lightBlue,colors.gray),height=6,width=2}
|
||||
-- local hcool = VerticalBar{parent=main,x=31,y=12,fg_bg=cpair(colors.orange,colors.gray),height=6,width=2}
|
||||
|
||||
-- annunciator --
|
||||
|
||||
local annunciator = Div{parent=main,x=34,y=3}
|
||||
|
||||
-- annunciator colors per IAEA-TECDOC-812 recommendations
|
||||
|
||||
-- connectivity/basic state
|
||||
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
|
||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
|
||||
---@todo auto control as info sent here
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Auto Control",colors=cpair(colors.blue,colors.gray)}
|
||||
|
||||
r_ps.subscribe("PLCOnline", plc_online.update)
|
||||
r_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
|
||||
r_ps.subscribe("status", r_active.update)
|
||||
|
||||
annunciator.line_break()
|
||||
|
||||
-- annunciator fields
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="High Startup Rate",colors=cpair(colors.yellow,colors.gray)}
|
||||
|
||||
r_ps.subscribe("ReactorSCRAM", r_scram.update)
|
||||
r_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
|
||||
r_ps.subscribe("RCPTrip", r_rtrip.update)
|
||||
r_ps.subscribe("RCSFlowLow", r_cflow.update)
|
||||
r_ps.subscribe("ReactorTempHigh", r_temp.update)
|
||||
r_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
|
||||
r_ps.subscribe("FuelInputRateLow", r_firl.update)
|
||||
r_ps.subscribe("WasteLineOcclusion", r_wloc.update)
|
||||
r_ps.subscribe("HighStartupRate", r_hsrt.update)
|
||||
|
||||
annunciator.line_break()
|
||||
|
||||
-- RPS
|
||||
local rps_trp = IndicatorLight{parent=annunciator,label="RPS Trip",colors=cpair(colors.red,colors.gray)}
|
||||
local rps_dmg = IndicatorLight{parent=annunciator,label="Damage Critical",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_exh = IndicatorLight{parent=annunciator,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_exw = IndicatorLight{parent=annunciator,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_tmp = IndicatorLight{parent=annunciator,label="High Core Temp",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_nof = IndicatorLight{parent=annunciator,label="No Fuel",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_noc = IndicatorLight{parent=annunciator,label="No Coolant",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_flt = IndicatorLight{parent=annunciator,label="PPM Fault",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_tmo = IndicatorLight{parent=annunciator,label="Timeout",colors=cpair(colors.yellow,colors.gray)}
|
||||
|
||||
r_ps.subscribe("rps_tripped", rps_trp.update)
|
||||
r_ps.subscribe("dmg_crit", rps_dmg.update)
|
||||
r_ps.subscribe("ex_hcool", rps_exh.update)
|
||||
r_ps.subscribe("ex_waste", rps_exw.update)
|
||||
r_ps.subscribe("high_temp", rps_tmp.update)
|
||||
r_ps.subscribe("no_fuel", rps_nof.update)
|
||||
r_ps.subscribe("no_cool", rps_noc.update)
|
||||
r_ps.subscribe("fault", rps_flt.update)
|
||||
r_ps.subscribe("timeout", rps_tmo.update)
|
||||
|
||||
annunciator.line_break()
|
||||
|
||||
-- cooling
|
||||
local c_brm = IndicatorLight{parent=annunciator,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_cfm = IndicatorLight{parent=annunciator,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_sfm = IndicatorLight{parent=annunciator,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_mwrf = IndicatorLight{parent=annunciator,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_tbnt = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
|
||||
|
||||
r_ps.subscribe("BoilRateMismatch", c_brm.update)
|
||||
r_ps.subscribe("CoolantFeedMismatch", c_cfm.update)
|
||||
r_ps.subscribe("SteamFeedMismatch", c_sfm.update)
|
||||
r_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
|
||||
r_ps.subscribe("TurbineTrip", c_tbnt.update)
|
||||
|
||||
annunciator.line_break()
|
||||
|
||||
-- machine-specific indicators
|
||||
if unit.num_boilers > 0 then
|
||||
TextBox{parent=main,x=32,y=34,text="B1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local b1_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
|
||||
end
|
||||
if unit.num_boilers > 1 then
|
||||
TextBox{parent=main,x=32,text="B2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local b2_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
b_ps[2].subscribe("HeatingRateLow", b2_hr.update)
|
||||
end
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
main.line_break()
|
||||
annunciator.line_break()
|
||||
end
|
||||
|
||||
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t1_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
|
||||
|
||||
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t1_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update)
|
||||
|
||||
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t1_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
|
||||
t_ps[1].subscribe("TurbineTrip", t1_trp.update)
|
||||
|
||||
main.line_break()
|
||||
annunciator.line_break()
|
||||
|
||||
if unit.num_turbines > 1 then
|
||||
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t2_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
|
||||
|
||||
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t2_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update)
|
||||
|
||||
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t2_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
|
||||
t_ps[2].subscribe("TurbineTrip", t2_trp.update)
|
||||
|
||||
main.line_break()
|
||||
annunciator.line_break()
|
||||
end
|
||||
|
||||
if unit.num_turbines > 2 then
|
||||
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t3_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
|
||||
|
||||
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t3_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update)
|
||||
|
||||
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
|
||||
local t3_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray)}
|
||||
t_ps[3].subscribe("TurbineTrip", t3_trp.update)
|
||||
|
||||
annunciator.line_break()
|
||||
end
|
||||
|
||||
---@todo radiation monitor
|
||||
IndicatorLight{parent=annunciator,label="Radiation Monitor",colors=cpair(colors.green,colors.gray)}
|
||||
IndicatorLight{parent=annunciator,label="Radiation Alarm",colors=cpair(colors.red,colors.gray)}
|
||||
DataIndicator{parent=main,x=34,y=51,label="",format="%10.1f",value=0,unit="mSv/h",lu_colors=lu_cpair,width=18,fg_bg=stat_fg_bg}
|
||||
|
||||
-- reactor controls --
|
||||
|
||||
StartButton{parent=main,x=12,y=44,callback=unit.start,fg_bg=scram_fg_bg}
|
||||
SCRAMButton{parent=main,x=22,y=44,callback=unit.scram,fg_bg=scram_fg_bg}
|
||||
|
||||
local burn_control = Div{parent=main,x=12,y=40,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
|
||||
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
|
||||
PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=set_burn}
|
||||
|
||||
local opts = {
|
||||
{
|
||||
text = "Auto",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.white, colors.gray)
|
||||
},
|
||||
{
|
||||
text = "Pu",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.lime)
|
||||
},
|
||||
{
|
||||
text = "Po",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.cyan)
|
||||
},
|
||||
{
|
||||
text = "AM",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.purple)
|
||||
}
|
||||
}
|
||||
|
||||
---@todo waste selection
|
||||
local waste_sel_f = function (s) print("waste: " .. s) end
|
||||
local waste_sel = Div{parent=main,x=2,y=48,width=29,height=2,fg_bg=cpair(colors.black, colors.white)}
|
||||
|
||||
MultiButton{parent=waste_sel,x=1,y=1,options=opts,callback=waste_sel_f,min_width=6,fg_bg=cpair(colors.black, colors.white)}
|
||||
TextBox{parent=waste_sel,text="Waste Processing",alignment=TEXT_ALIGN.CENTER,x=1,y=1,height=1}
|
||||
|
||||
---@fixme test code
|
||||
main.line_break()
|
||||
ColorMap{parent=main,x=2,y=51}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return init
|
||||
176
coordinator/ui/components/unit_overview.lua
Normal file
176
coordinator/ui/components/unit_overview.lua
Normal file
@@ -0,0 +1,176 @@
|
||||
--
|
||||
-- Basic Unit Overview
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local reactor_view = require("coordinator.ui.components.reactor")
|
||||
local boiler_view = require("coordinator.ui.components.boiler")
|
||||
local turbine_view = require("coordinator.ui.components.turbine")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
local pipe = core.graphics.pipe
|
||||
|
||||
-- make a new unit overview window
|
||||
---@param parent graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param unit ioctl_entry unit database entry
|
||||
local function make(parent, x, y, unit)
|
||||
local height = 0
|
||||
local num_boilers = #unit.boiler_data_tbl
|
||||
local num_turbines = #unit.turbine_data_tbl
|
||||
|
||||
assert(num_boilers >= 0 and num_boilers <= 2, "minimum 0 boilers, maximum 2 boilers")
|
||||
assert(num_turbines >= 1 and num_turbines <= 3, "minimum 1 turbine, maximum 3 turbines")
|
||||
|
||||
if num_boilers == 0 and num_turbines == 1 then
|
||||
height = 9
|
||||
elseif num_boilers == 1 and num_turbines <= 2 then
|
||||
height = 17
|
||||
else
|
||||
height = 25
|
||||
end
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
|
||||
-- unit header message
|
||||
TextBox{parent=root,text="Unit #" .. unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
-------------
|
||||
-- REACTOR --
|
||||
-------------
|
||||
|
||||
reactor_view(root, 1, 3, unit.reactor_data, unit.reactor_ps)
|
||||
|
||||
if num_boilers > 0 then
|
||||
local coolant_pipes = {}
|
||||
|
||||
if num_boilers >= 2 then
|
||||
table.insert(coolant_pipes, pipe(0, 0, 11, 12, colors.lightBlue))
|
||||
end
|
||||
|
||||
table.insert(coolant_pipes, pipe(0, 0, 11, 3, colors.lightBlue))
|
||||
table.insert(coolant_pipes, pipe(2, 0, 11, 2, colors.orange))
|
||||
|
||||
if num_boilers >= 2 then
|
||||
table.insert(coolant_pipes, pipe(2, 0, 11, 11, colors.orange))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=4,y=10,pipes=coolant_pipes,bg=colors.lightGray}
|
||||
end
|
||||
|
||||
-------------
|
||||
-- BOILERS --
|
||||
-------------
|
||||
|
||||
if num_boilers >= 1 then boiler_view(root, 16, 11, unit.boiler_ps_tbl[1]) end
|
||||
if num_boilers >= 2 then boiler_view(root, 16, 19, unit.boiler_ps_tbl[2]) end
|
||||
|
||||
--------------
|
||||
-- TURBINES --
|
||||
--------------
|
||||
|
||||
local t_idx = 1
|
||||
local no_boilers = num_boilers == 0
|
||||
|
||||
if (num_turbines >= 3) or no_boilers or (num_boilers == 1 and num_turbines >= 2) then
|
||||
turbine_view(root, 58, 3, unit.turbine_ps_tbl[t_idx])
|
||||
t_idx = t_idx + 1
|
||||
end
|
||||
|
||||
if (num_turbines >= 1 and not no_boilers) or num_turbines >= 2 then
|
||||
turbine_view(root, 58, 11, unit.turbine_ps_tbl[t_idx])
|
||||
t_idx = t_idx + 1
|
||||
end
|
||||
|
||||
if (num_turbines >= 2 and num_boilers >= 2) or num_turbines >= 3 then
|
||||
turbine_view(root, 58, 19, unit.turbine_ps_tbl[t_idx])
|
||||
end
|
||||
|
||||
local steam_pipes_b = {}
|
||||
|
||||
if no_boilers then
|
||||
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
|
||||
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
|
||||
|
||||
if num_turbines >= 2 then
|
||||
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
|
||||
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
|
||||
end
|
||||
|
||||
if num_turbines >= 3 then
|
||||
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
|
||||
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
|
||||
end
|
||||
else
|
||||
-- boiler side pipes
|
||||
local steam_pipes_a = {
|
||||
-- boiler 1 steam/water pipes
|
||||
pipe(0, 1, 6, 1, colors.white, false, true), -- steam boiler 1 to turbine junction
|
||||
pipe(0, 2, 6, 2, colors.blue, false, true) -- water boiler 1 to turbine junction
|
||||
}
|
||||
|
||||
if num_boilers >= 2 then
|
||||
-- boiler 2 steam/water pipes
|
||||
table.insert(steam_pipes_a, pipe(0, 9, 6, 9, colors.white, false, true)) -- steam boiler 2 to turbine junction
|
||||
table.insert(steam_pipes_a, pipe(0, 10, 6, 10, colors.blue, false, true)) -- water boiler 2 to turbine junction
|
||||
end
|
||||
|
||||
-- turbine side pipes
|
||||
|
||||
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
|
||||
table.insert(steam_pipes_b, pipe(0, 9, 1, 2, colors.white, false, true)) -- steam boiler 1 to turbine 1 junction start
|
||||
table.insert(steam_pipes_b, pipe(1, 1, 3, 1, colors.white, false, false)) -- steam boiler 1 to turbine 1 junction end
|
||||
end
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 9, 3, 9, colors.white, false, true)) -- steam boiler 1 to turbine 2
|
||||
|
||||
if num_turbines >= 3 or (num_boilers == 1 and num_turbines == 2) then
|
||||
table.insert(steam_pipes_b, pipe(0, 10, 2, 3, colors.blue, false, true)) -- water boiler 1 to turbine 1 junction start
|
||||
table.insert(steam_pipes_b, pipe(2, 2, 3, 2, colors.blue, false, false)) -- water boiler 1 to turbine 1 junction end
|
||||
end
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 10, 3, 10, colors.blue, false, true)) -- water boiler 1 to turbine 2
|
||||
|
||||
if num_turbines >= 3 or (num_turbines >= 2 and num_boilers >= 2) then
|
||||
if num_boilers >= 2 then
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
else
|
||||
table.insert(steam_pipes_b, pipe(1, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
|
||||
table.insert(steam_pipes_b, pipe(1, 17, 3, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
|
||||
|
||||
table.insert(steam_pipes_b, pipe(2, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
|
||||
table.insert(steam_pipes_b, pipe(2, 18, 3, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
end
|
||||
elseif num_turbines == 1 and num_boilers >= 2 then
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 1, 9, colors.white, false, true)) -- steam boiler 2 to turbine 2 junction
|
||||
table.insert(steam_pipes_b, pipe(0, 17, 1, 17, colors.white, false, true)) -- steam boiler 2 to turbine 3
|
||||
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 10, colors.blue, false, true)) -- water boiler 2 to turbine 3
|
||||
table.insert(steam_pipes_b, pipe(0, 18, 2, 18, colors.blue, false, true)) -- water boiler 2 to turbine 2 junction
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=47,y=11,pipes=steam_pipes_a,bg=colors.lightGray}
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=54,y=3,pipes=steam_pipes_b,bg=colors.lightGray}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return make
|
||||
33
coordinator/ui/components/unit_waiting.lua
Normal file
33
coordinator/ui/components/unit_waiting.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
--
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.waiting")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
|
||||
-- create a unit waiting view
|
||||
---@param parent graphics_element parent
|
||||
---@param y integer y offset
|
||||
local function init(parent, y)
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=1,y=y,height=5}
|
||||
|
||||
local waiting_x = math.floor(parent.width() / 2) - 2
|
||||
|
||||
TextBox{parent=root,text="Waiting for status...",alignment=TEXT_ALIGN.CENTER,y=1,height=1,fg_bg=cpair(colors.black,style.root.bkg)}
|
||||
WaitingAnim{parent=root,x=waiting_x,y=3,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return init
|
||||
45
coordinator/ui/dialog.lua
Normal file
45
coordinator/ui/dialog.lua
Normal file
@@ -0,0 +1,45 @@
|
||||
local completion = require("cc.completion")
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local dialog = {}
|
||||
|
||||
function dialog.ask_y_n(question, default)
|
||||
print(question)
|
||||
|
||||
if default == true then
|
||||
print(" (Y/n)? ")
|
||||
else
|
||||
print(" (y/N)? ")
|
||||
end
|
||||
|
||||
local response = read(nil, nil)
|
||||
|
||||
if response == "" then
|
||||
return default
|
||||
elseif response == "Y" or response == "y" then
|
||||
return true
|
||||
elseif response == "N" or response == "n" then
|
||||
return false
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function dialog.ask_options(options, cancel)
|
||||
print("> ")
|
||||
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
|
||||
|
||||
if response == cancel then return false end
|
||||
|
||||
if util.table_contains(options, response) then
|
||||
return response
|
||||
else return nil end
|
||||
end
|
||||
|
||||
return dialog
|
||||
47
coordinator/ui/layout/main_view.lua
Normal file
47
coordinator/ui/layout/main_view.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_overview = require("coordinator.ui.components.unit_overview")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
-- create new main view
|
||||
---@param monitor table main viewscreen
|
||||
local function init(monitor)
|
||||
local main = DisplayBox{window=monitor,fg_bg=style.root}
|
||||
|
||||
-- window header message
|
||||
TextBox{parent=main,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
|
||||
|
||||
-- unit overviews
|
||||
if db.facility.num_units >= 1 then uo_1 = unit_overview(main, 2, 3, db.units[1]) end
|
||||
if db.facility.num_units >= 2 then uo_2 = unit_overview(main, 84, 3, db.units[2]) end
|
||||
|
||||
if db.facility.num_units >= 3 then
|
||||
-- base offset 3, spacing 1, max height of units 1 and 2
|
||||
local row_2_offset = 3 + 1 + math.max(uo_1.height(), uo_2.height())
|
||||
|
||||
uo_3 = unit_overview(main, 2, row_2_offset, db.units[3])
|
||||
if db.facility.num_units == 4 then uo_4 = unit_overview(main, 84, row_2_offset, db.units[4]) end
|
||||
end
|
||||
|
||||
-- command & control
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return init
|
||||
26
coordinator/ui/layout/unit_view.lua
Normal file
26
coordinator/ui/layout/unit_view.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
--
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local tcallbackdsp = require("scada-common.tcallbackdsp")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_detail = require("coordinator.ui.components.unit_detail")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
-- create a unit view
|
||||
---@param monitor table
|
||||
---@param id integer
|
||||
local function init(monitor, id)
|
||||
local main = DisplayBox{window=monitor,fg_bg=style.root}
|
||||
|
||||
unit_detail(main, id)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return init
|
||||
101
coordinator/ui/style.lua
Normal file
101
coordinator/ui/style.lua
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = {}
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.lime, hex = 0x64dd20 },
|
||||
{ c = colors.green, hex = 0x4aee8a },
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xf26ba2 },
|
||||
{ c = colors.magenta, hex = 0xf9488a },
|
||||
-- { c = colors.white, hex = 0xf0f0f0 },
|
||||
{ c = colors.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
-- { c = colors.brown, hex = 0x7f664c }
|
||||
}
|
||||
|
||||
-- MAIN LAYOUT --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "PLC OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "DISABLED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "SCRAMMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "PLC FAULT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.boiler = {
|
||||
-- boiler states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.turbine = {
|
||||
-- turbine states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.red),
|
||||
text = "TRIP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
146
graphics/core.lua
Normal file
146
graphics/core.lua
Normal file
@@ -0,0 +1,146 @@
|
||||
--
|
||||
-- Graphics Core Functions and Objects
|
||||
--
|
||||
|
||||
local core = {}
|
||||
|
||||
local events = {}
|
||||
|
||||
---@class monitor_touch
|
||||
---@field monitor string
|
||||
---@field x integer
|
||||
---@field y integer
|
||||
|
||||
-- create a new touch event definition
|
||||
---@param monitor string
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return monitor_touch
|
||||
function events.touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
x = x,
|
||||
y = y
|
||||
}
|
||||
end
|
||||
|
||||
core.events = events
|
||||
|
||||
local graphics = {}
|
||||
|
||||
---@alias TEXT_ALIGN integer
|
||||
graphics.TEXT_ALIGN = {
|
||||
LEFT = 1,
|
||||
CENTER = 2,
|
||||
RIGHT = 3
|
||||
}
|
||||
|
||||
---@class graphics_border
|
||||
---@field width integer
|
||||
---@field color color
|
||||
---@field even boolean
|
||||
|
||||
---@alias element_id string|integer
|
||||
|
||||
-- create a new border definition
|
||||
---@param width integer border width
|
||||
---@param color color border color
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
---@return graphics_border
|
||||
function graphics.border(width, color, even)
|
||||
return {
|
||||
width = width,
|
||||
color = color,
|
||||
even = even or false -- convert nil to false
|
||||
}
|
||||
end
|
||||
|
||||
---@class graphics_frame
|
||||
---@field x integer
|
||||
---@field y integer
|
||||
---@field w integer
|
||||
---@field h integer
|
||||
|
||||
-- create a new graphics frame definition
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@return graphics_frame
|
||||
function graphics.gframe(x, y, w, h)
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
w = w,
|
||||
h = h
|
||||
}
|
||||
end
|
||||
|
||||
---@class cpair
|
||||
---@field color_a color
|
||||
---@field color_b color
|
||||
---@field blit_a string
|
||||
---@field blit_b string
|
||||
---@field fgd color
|
||||
---@field bkg color
|
||||
---@field blit_fgd string
|
||||
---@field blit_bkg string
|
||||
|
||||
-- create a new color pair definition
|
||||
---@param a color
|
||||
---@param b color
|
||||
---@return cpair
|
||||
function graphics.cpair(a, b)
|
||||
return {
|
||||
-- color pairs
|
||||
color_a = a,
|
||||
color_b = b,
|
||||
blit_a = colors.toBlit(a),
|
||||
blit_b = colors.toBlit(b),
|
||||
-- aliases
|
||||
fgd = a,
|
||||
bkg = b,
|
||||
blit_fgd = colors.toBlit(a),
|
||||
blit_bkg = colors.toBlit(b)
|
||||
}
|
||||
end
|
||||
|
||||
---@class pipe
|
||||
---@field x1 integer starting x, origin is 0
|
||||
---@field y1 integer starting y, origin is 0
|
||||
---@field x2 integer ending x, origin is 0
|
||||
---@field y2 integer ending y, origin is 0
|
||||
---@field w integer width
|
||||
---@field h integer height
|
||||
---@field color color pipe color
|
||||
---@field thin boolean true for 1 subpixel, false (default) for 2
|
||||
---@field align_tr boolean false to align bottom left (default), true to align top right
|
||||
|
||||
-- create a new pipe
|
||||
--
|
||||
-- note: pipe coordinate origin is (0, 0)
|
||||
---@param x1 integer starting x, origin is 0
|
||||
---@param y1 integer starting y, origin is 0
|
||||
---@param x2 integer ending x, origin is 0
|
||||
---@param y2 integer ending y, origin is 0
|
||||
---@param color color pipe color
|
||||
---@param thin? boolean true for 1 subpixel, false (default) for 2
|
||||
---@param align_tr? boolean false to align bottom left (default), true to align top right
|
||||
---@return pipe
|
||||
function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||
return {
|
||||
x1 = x1,
|
||||
y1 = y1,
|
||||
x2 = x2,
|
||||
y2 = y2,
|
||||
w = math.abs(x2 - x1) + 1,
|
||||
h = math.abs(y2 - y1) + 1,
|
||||
color = color,
|
||||
thin = thin or false,
|
||||
align_tr = align_tr or false
|
||||
}
|
||||
end
|
||||
|
||||
core.graphics = graphics
|
||||
|
||||
return core
|
||||
342
graphics/element.lua
Normal file
342
graphics/element.lua
Normal file
@@ -0,0 +1,342 @@
|
||||
--
|
||||
-- Generic Graphics Element
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local element = {}
|
||||
|
||||
---@class graphics_args_generic
|
||||
---@field window? table
|
||||
---@field parent? graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer next line if omitted
|
||||
---@field offset_x? integer 0 if omitted
|
||||
---@field offset_y? integer 0 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- a base graphics element, should not be created on its own
|
||||
---@param args graphics_args_generic arguments
|
||||
function element.new(args)
|
||||
local self = {
|
||||
id = -1,
|
||||
elem_type = debug.getinfo(2).name,
|
||||
define_completed = false,
|
||||
p_window = nil, ---@type table
|
||||
position = { x = 1, y = 1 },
|
||||
child_offset = { x = 0, y = 0 },
|
||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1},
|
||||
next_y = 1,
|
||||
children = {},
|
||||
mt = {}
|
||||
}
|
||||
|
||||
---@class graphics_template
|
||||
local protected = {
|
||||
value = nil, ---@type any
|
||||
window = nil, ---@type table
|
||||
fg_bg = core.graphics.cpair(colors.white, colors.black),
|
||||
frame = core.graphics.gframe(1, 1, 1, 1)
|
||||
}
|
||||
|
||||
-- element as string
|
||||
function self.mt.__tostring()
|
||||
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
|
||||
end
|
||||
|
||||
---@class graphics_element
|
||||
local public = {}
|
||||
|
||||
setmetatable(public, self.mt)
|
||||
|
||||
-------------------------
|
||||
-- PROTECTED FUNCTIONS --
|
||||
-------------------------
|
||||
|
||||
-- prepare the template
|
||||
---@param offset_x integer x offset
|
||||
---@param offset_y integer y offset
|
||||
---@param next_y integer next line if no y was provided
|
||||
function protected.prepare_template(offset_x, offset_y, next_y)
|
||||
-- get frame coordinates/size
|
||||
if args.gframe ~= nil then
|
||||
protected.frame.x = args.gframe.x
|
||||
protected.frame.y = args.gframe.y
|
||||
protected.frame.w = args.gframe.w
|
||||
protected.frame.h = args.gframe.h
|
||||
else
|
||||
local w, h = self.p_window.getSize()
|
||||
protected.frame.x = args.x or 1
|
||||
protected.frame.y = args.y or next_y
|
||||
protected.frame.w = args.width or w
|
||||
protected.frame.h = args.height or h
|
||||
end
|
||||
|
||||
-- inner offsets
|
||||
if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
|
||||
if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
|
||||
|
||||
-- adjust window frame if applicable
|
||||
local f = protected.frame
|
||||
local x = f.x
|
||||
local y = f.y
|
||||
|
||||
-- apply offsets
|
||||
if args.parent ~= nil then
|
||||
-- constrain to parent inner width/height
|
||||
local w, h = self.p_window.getSize()
|
||||
f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1)))
|
||||
f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1)))
|
||||
|
||||
-- offset x/y
|
||||
f.x = x + offset_x
|
||||
f.y = y + offset_y
|
||||
end
|
||||
|
||||
-- check frame
|
||||
assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1")
|
||||
assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1")
|
||||
assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1")
|
||||
assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: frame height not >= 1")
|
||||
|
||||
-- create window
|
||||
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true)
|
||||
|
||||
-- init colors
|
||||
if args.fg_bg ~= nil then
|
||||
protected.fg_bg = args.fg_bg
|
||||
elseif args.parent ~= nil then
|
||||
protected.fg_bg = args.parent.get_fg_bg()
|
||||
end
|
||||
|
||||
-- set colors
|
||||
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
||||
protected.window.setTextColor(protected.fg_bg.fgd)
|
||||
protected.window.clear()
|
||||
|
||||
-- record position
|
||||
self.position.x, self.position.y = protected.window.getPosition()
|
||||
|
||||
-- calculate bounds
|
||||
self.bounds.x1 = self.position.x
|
||||
self.bounds.x2 = self.position.x + f.w - 1
|
||||
self.bounds.y1 = self.position.y
|
||||
self.bounds.y2 = self.position.y + f.h - 1
|
||||
end
|
||||
|
||||
-- handle a touch event
|
||||
---@param event table monitor_touch event
|
||||
function protected.handle_touch(event)
|
||||
end
|
||||
|
||||
-- handle data value changes
|
||||
function protected.on_update(...)
|
||||
end
|
||||
|
||||
-- get value
|
||||
function protected.get_value()
|
||||
return protected.value
|
||||
end
|
||||
|
||||
-- set value
|
||||
---@param value any value to set
|
||||
function protected.set_value(value)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- custom recolor command, varies by element if implemented
|
||||
---@vararg cpair|color color(s)
|
||||
function protected.recolor(...)
|
||||
end
|
||||
|
||||
-- custom resize command, varies by element if implemented
|
||||
---@vararg integer sizing
|
||||
function protected.resize(...)
|
||||
end
|
||||
|
||||
-- start animations
|
||||
function protected.start_anim()
|
||||
end
|
||||
|
||||
-- stop animations
|
||||
function protected.stop_anim()
|
||||
end
|
||||
|
||||
-- get public interface
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.get() return public, self.id end
|
||||
|
||||
-----------
|
||||
-- SETUP --
|
||||
-----------
|
||||
|
||||
-- get the parent window
|
||||
self.p_window = args.window
|
||||
if self.p_window == nil and args.parent ~= nil then
|
||||
self.p_window = args.parent.window()
|
||||
end
|
||||
|
||||
-- check window
|
||||
assert(self.p_window, "graphics.element{" .. self.elem_type .. "}: no parent window provided")
|
||||
|
||||
-- prepare the template
|
||||
if args.parent == nil then
|
||||
protected.prepare_template(0, 0, 1)
|
||||
else
|
||||
self.id = args.parent.__add_child(args.id, protected)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- PUBLIC FUNCTIONS --
|
||||
----------------------
|
||||
|
||||
-- get the window object
|
||||
function public.window() return protected.window end
|
||||
|
||||
-- CHILD ELEMENTS --
|
||||
|
||||
-- add a child element
|
||||
---@param key string|nil id
|
||||
---@param child graphics_template
|
||||
---@return integer|string key
|
||||
function public.__add_child(key, child)
|
||||
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
|
||||
|
||||
self.next_y = child.frame.y + child.frame.h
|
||||
|
||||
local child_element = child.get()
|
||||
|
||||
if key == nil then
|
||||
table.insert(self.children, child_element)
|
||||
return #self.children
|
||||
else
|
||||
self.children[key] = child_element
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
-- get a child element
|
||||
---@return graphics_element
|
||||
function public.get_child(key) return self.children[key] end
|
||||
|
||||
-- remove child
|
||||
---@param key string|integer
|
||||
function public.remove(key) self.children[key] = nil end
|
||||
|
||||
-- attempt to get a child element by ID (does not include this element itself)
|
||||
---@param id element_id
|
||||
---@return graphics_element|nil element
|
||||
function public.get_element_by_id(id)
|
||||
if self.children[id] == nil then
|
||||
for _, child in pairs(self.children) do
|
||||
local elem = child.get_element_by_id(id)
|
||||
if elem ~= nil then return elem end
|
||||
end
|
||||
else
|
||||
return self.children[id]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- AUTO-PLACEMENT --
|
||||
|
||||
-- skip a line for automatically placed elements
|
||||
function public.line_break() self.next_y = self.next_y + 1 end
|
||||
|
||||
-- PROPERTIES --
|
||||
|
||||
-- get the foreground/background colors
|
||||
---@return cpair fg_bg
|
||||
function public.get_fg_bg() return protected.fg_bg end
|
||||
|
||||
-- get element width
|
||||
---@return integer width
|
||||
function public.width()
|
||||
return protected.frame.w
|
||||
end
|
||||
|
||||
-- get element height
|
||||
---@return integer height
|
||||
function public.height()
|
||||
return protected.frame.h
|
||||
end
|
||||
|
||||
-- get the element value
|
||||
---@return any value
|
||||
function public.get_value()
|
||||
return protected.get_value()
|
||||
end
|
||||
|
||||
-- set the element value
|
||||
---@param value any new value
|
||||
function public.set_value(value)
|
||||
protected.set_value(value)
|
||||
end
|
||||
|
||||
-- resize attributes of the element value if supported
|
||||
---@vararg number dimensions (element specific)
|
||||
function public.resize(...)
|
||||
protected.resize(...)
|
||||
end
|
||||
|
||||
-- FUNCTION CALLBACKS --
|
||||
|
||||
-- handle a monitor touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function public.handle_touch(event)
|
||||
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
|
||||
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
|
||||
|
||||
if in_x and in_y then
|
||||
local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
|
||||
|
||||
-- handle the touch event, transformed into the window frame
|
||||
protected.handle_touch(event_T)
|
||||
|
||||
-- pass on touch event to children
|
||||
for _, val in pairs(self.children) do val.handle_touch(event_T) end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the element given new data
|
||||
---@vararg any new data
|
||||
function public.update(...)
|
||||
protected.on_update(...)
|
||||
end
|
||||
|
||||
-- VISIBILITY --
|
||||
|
||||
-- show the element
|
||||
function public.show()
|
||||
protected.window.setVisible(true)
|
||||
protected.start_anim()
|
||||
|
||||
for i = 1, #self.children do
|
||||
self.children[i].show()
|
||||
end
|
||||
end
|
||||
|
||||
-- hide the element
|
||||
function public.hide()
|
||||
protected.stop_anim()
|
||||
for i = 1, #self.children do
|
||||
self.children[i].hide()
|
||||
end
|
||||
|
||||
protected.window.setVisible(false)
|
||||
end
|
||||
|
||||
-- re-draw the element
|
||||
function public.redraw()
|
||||
protected.window.redraw()
|
||||
end
|
||||
|
||||
return protected
|
||||
end
|
||||
|
||||
return element
|
||||
108
graphics/elements/animations/waiting.lua
Normal file
108
graphics/elements/animations/waiting.lua
Normal file
@@ -0,0 +1,108 @@
|
||||
-- Loading/Waiting Animation Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class waiting_args
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new waiting animation element
|
||||
---@param args waiting_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function waiting(args)
|
||||
local state = 0
|
||||
local run_animation = false
|
||||
|
||||
args.width = 4
|
||||
args.height = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local blit_fg = e.fg_bg.blit_fgd
|
||||
local blit_bg = e.fg_bg.blit_bkg
|
||||
local blit_fg_2x = e.fg_bg.blit_fgd .. e.fg_bg.blit_fgd
|
||||
local blit_bg_2x = e.fg_bg.blit_bkg .. e.fg_bg.blit_bkg
|
||||
|
||||
-- tick the animation
|
||||
local function animate()
|
||||
e.window.clear()
|
||||
|
||||
if state >= 0 and state < 7 then
|
||||
-- top
|
||||
e.window.setCursorPos(1 + math.floor(state / 2), 1)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
|
||||
-- bottom
|
||||
e.window.setCursorPos(4 - math.ceil(state / 2), 3)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
else
|
||||
local st = state - 7
|
||||
|
||||
-- right
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
|
||||
-- left
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(1, 3 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
else
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
end
|
||||
|
||||
state = state + 1
|
||||
if state >= 12 then state = 0 end
|
||||
|
||||
if run_animation then
|
||||
tcd.dispatch(0.5, animate)
|
||||
end
|
||||
end
|
||||
|
||||
-- start the animation
|
||||
function e.start_anim()
|
||||
run_animation = true
|
||||
animate()
|
||||
end
|
||||
|
||||
-- stop the animation
|
||||
function e.stop_anim()
|
||||
run_animation = false
|
||||
end
|
||||
|
||||
e.start_anim()
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return waiting
|
||||
33
graphics/elements/colormap.lua
Normal file
33
graphics/elements/colormap.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Color Map Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class colormap_args
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
|
||||
-- new color map
|
||||
---@param args colormap_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function colormap(args)
|
||||
local bkg = "008877FFCCEE114455DD9933BBAA2266"
|
||||
local spaces = util.spaces(32)
|
||||
|
||||
args.width = 32
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw color map
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(spaces, bkg, bkg)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return colormap
|
||||
121
graphics/elements/controls/multi_button.lua
Normal file
121
graphics/elements/controls/multi_button.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
---@class button_option
|
||||
---@field text string
|
||||
---@field fg_bg cpair
|
||||
---@field active_fg_bg cpair
|
||||
---@field _lpad integer automatically calculated left pad
|
||||
---@field _start_x integer starting touch x range (inclusive)
|
||||
---@field _end_x integer ending touch x range (inclusive)
|
||||
|
||||
---@class multi_button_args
|
||||
---@field options table button options
|
||||
---@field callback function function to call on touch
|
||||
---@field default? boolean default state, defaults to options[1]
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new multi button (latch selection, exclusively one button at a time)
|
||||
---@param args multi_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multi_button(args)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 3
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
if string.len(opt.text) > max_width then
|
||||
max_width = string.len(opt.text)
|
||||
end
|
||||
end
|
||||
|
||||
local button_width = math.max(max_width, args.min_width or 1)
|
||||
|
||||
args.width = (button_width * #args.options) + #args.options + 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- button state (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- calculate required button information
|
||||
local next_x = 2
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
local w = string.len(opt.text)
|
||||
|
||||
opt._lpad = math.floor((e.frame.w - w) / 2)
|
||||
opt._start_x = next_x
|
||||
opt._end_x = next_x + button_width - 1
|
||||
|
||||
next_x = next_x + (button_width + 1)
|
||||
end
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
e.window.setCursorPos(opt._start_x, 2)
|
||||
|
||||
if e.value == i then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(opt.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(opt.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(opt.text, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function e.handle_touch(event)
|
||||
-- determine what was pressed
|
||||
if event.y == 2 then
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
if event.x >= opt._start_x and event.x <= opt._end_x then
|
||||
e.value = i
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return multi_button
|
||||
86
graphics/elements/controls/push_button.lua
Normal file
86
graphics/elements/controls/push_button.lua
Normal file
@@ -0,0 +1,86 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class push_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new push button
|
||||
---@param args push_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function push_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
args.min_width = args.min_width or 0
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
args.width = math.max(text_width + 2, args.min_width)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- draw the button
|
||||
local function draw()
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
if args.active_fg_bg ~= nil then
|
||||
-- show as pressed
|
||||
e.value = true
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
|
||||
-- show as unpressed in 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
end)
|
||||
end
|
||||
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_touch(core.events.touch("", 1, 1)) end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return push_button
|
||||
76
graphics/elements/controls/scram_button.lua
Normal file
76
graphics/elements/controls/scram_button.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
-- SCRAM Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class scram_button_args
|
||||
---@field callback function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new scram button
|
||||
---@param args scram_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function scram_button(args)
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.scram_button: callback is a required field")
|
||||
|
||||
-- static dimensions
|
||||
args.height = 3
|
||||
args.width = 9
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write("SCRAM")
|
||||
|
||||
-- draw border
|
||||
|
||||
-- top
|
||||
e.window.setTextColor(colors.yellow)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write("\x99\x89\x89\x89\x89\x89\x89\x89\x99")
|
||||
|
||||
-- center left
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(colors.yellow)
|
||||
e.window.write("\x99")
|
||||
|
||||
-- center right
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(colors.yellow)
|
||||
e.window.setCursorPos(9, 2)
|
||||
e.window.write("\x99")
|
||||
|
||||
-- bottom
|
||||
e.window.setTextColor(colors.yellow)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write("\x99\x98\x98\x98\x98\x98\x98\x98\x99")
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_touch(core.events.touch("", 1, 1)) end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return scram_button
|
||||
145
graphics/elements/controls/spinbox_numeric.lua
Normal file
145
graphics/elements/controls/spinbox_numeric.lua
Normal file
@@ -0,0 +1,145 @@
|
||||
-- Spinbox Numeric Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
---@class spinbox_args
|
||||
---@field default? number default value, defaults to 0.0
|
||||
---@field whole_num_precision integer number of whole number digits
|
||||
---@field fractional_precision integer number of fractional digits
|
||||
---@field arrow_fg_bg cpair arrow foreground/background colors
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new spinbox control (minimum value is 0)
|
||||
---@param args spinbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function spinbox(args)
|
||||
-- properties
|
||||
local digits = {}
|
||||
local wn_prec = args.whole_num_precision
|
||||
local fr_prec = args.fractional_precision
|
||||
|
||||
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
|
||||
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
||||
|
||||
local fmt = "%" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
||||
local fmt_init = "%0" .. (wn_prec + fr_prec + 1) .. "." .. fr_prec .. "f"
|
||||
local dec_point_x = args.whole_num_precision + 1
|
||||
|
||||
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field")
|
||||
|
||||
-- determine widths
|
||||
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
|
||||
args.height = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- set initial value
|
||||
e.value = args.default or 0.0
|
||||
|
||||
local initial_str = util.sprintf(fmt_init, e.value)
|
||||
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
initial_str:gsub("%d", function (char) table.insert(digits, char) end)
|
||||
|
||||
-- draw the arrows
|
||||
e.window.setBackgroundColor(args.arrow_fg_bg.bkg)
|
||||
e.window.setTextColor(args.arrow_fg_bg.fgd)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(util.strrep("\x1e", wn_prec))
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write(util.strrep("\x1f", wn_prec))
|
||||
if fr_prec > 0 then
|
||||
e.window.setCursorPos(1 + wn_prec, 1)
|
||||
e.window.write(" " .. util.strrep("\x1e", fr_prec))
|
||||
e.window.setCursorPos(1 + wn_prec, 3)
|
||||
e.window.write(" " .. util.strrep("\x1f", fr_prec))
|
||||
end
|
||||
|
||||
-- zero the value
|
||||
local function zero()
|
||||
for i = 1, #digits do digits[i] = 0 end
|
||||
e.value = 0
|
||||
end
|
||||
|
||||
-- print out the current value
|
||||
local function show_num()
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.write(util.sprintf(fmt, e.value))
|
||||
end
|
||||
|
||||
-- update the value per digits table
|
||||
local function update_value()
|
||||
e.value = 0
|
||||
for i = 1, #digits do
|
||||
local pow = math.abs(wn_prec - i)
|
||||
if i <= wn_prec then
|
||||
e.value = e.value + (digits[i] * (10 ^ pow))
|
||||
else
|
||||
e.value = e.value + (digits[i] * (10 ^ -pow))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- enforce numeric limits
|
||||
local function enforce_limits()
|
||||
-- min 0
|
||||
if e.value < 0 then
|
||||
zero()
|
||||
-- max printable
|
||||
elseif string.len(util.sprintf(fmt, e.value)) > args.width then
|
||||
-- max out
|
||||
for i = 1, #digits do digits[i] = 9 end
|
||||
|
||||
-- re-update value
|
||||
update_value()
|
||||
end
|
||||
end
|
||||
|
||||
-- update value and show
|
||||
local function parse_and_show()
|
||||
update_value()
|
||||
enforce_limits()
|
||||
show_num()
|
||||
end
|
||||
|
||||
-- init with the default value
|
||||
show_num()
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
function e.handle_touch(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if event.x ~= dec_point_x then
|
||||
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
|
||||
if event.y == 1 then
|
||||
-- increment
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.y == 3 then
|
||||
-- decrement
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
parse_and_show()
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val number number to show
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
parse_and_show()
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return spinbox
|
||||
76
graphics/elements/controls/start_button.lua
Normal file
76
graphics/elements/controls/start_button.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
-- SCRAM Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class start_button_args
|
||||
---@field callback function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new start button
|
||||
---@param args start_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function start_button(args)
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.start_button: callback is a required field")
|
||||
|
||||
-- static dimensions
|
||||
args.height = 3
|
||||
args.width = 9
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write("START")
|
||||
|
||||
-- draw border
|
||||
|
||||
-- top
|
||||
e.window.setTextColor(colors.orange)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write("\x99\x89\x89\x89\x89\x89\x89\x89\x99")
|
||||
|
||||
-- center left
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(colors.orange)
|
||||
e.window.write("\x99")
|
||||
|
||||
-- center right
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(colors.orange)
|
||||
e.window.setCursorPos(9, 2)
|
||||
e.window.write("\x99")
|
||||
|
||||
-- bottom
|
||||
e.window.setTextColor(colors.orange)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write("\x99\x98\x98\x98\x98\x98\x98\x98\x99")
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_touch(core.events.touch("", 1, 1)) end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return start_button
|
||||
88
graphics/elements/controls/switch_button.lua
Normal file
88
graphics/elements/controls/switch_button.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class switch_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field default? boolean default state, defaults to off (false)
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field active_fg_bg cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new switch button (latch high/low)
|
||||
---@param args switch_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function switch_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
|
||||
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine widths
|
||||
local text_width = string.len(args.text)
|
||||
args.width = math.max(text_width + 2, args.min_width)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- button state (convert nil to false if missing)
|
||||
e.value = args.default or false
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2)
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- show the button state
|
||||
local function draw_state()
|
||||
if e.value then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw_state()
|
||||
|
||||
-- handle touch
|
||||
---@param event monitor_touch monitor touch event
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function e.handle_touch(event)
|
||||
-- toggle state
|
||||
e.value = not e.value
|
||||
draw_state()
|
||||
|
||||
-- call the touch callback with state
|
||||
args.callback(e.value)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
-- set state
|
||||
e.value = val
|
||||
draw_state()
|
||||
|
||||
-- call the touch callback with state
|
||||
args.callback(e.value)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return switch_button
|
||||
21
graphics/elements/displaybox.lua
Normal file
21
graphics/elements/displaybox.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Root Display Box Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class displaybox_args
|
||||
---@field window table
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new root display box
|
||||
---@param args displaybox_args
|
||||
local function displaybox(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).get()
|
||||
end
|
||||
|
||||
return displaybox
|
||||
23
graphics/elements/div.lua
Normal file
23
graphics/elements/div.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Div (Division, like in HTML) Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class div_args
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new div element
|
||||
---@param args div_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function div(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).get()
|
||||
end
|
||||
|
||||
return div
|
||||
165
graphics/elements/indicators/coremap.lua
Normal file
165
graphics/elements/indicators/coremap.lua
Normal file
@@ -0,0 +1,165 @@
|
||||
-- Reactor Core View Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class core_map_args
|
||||
---@field reactor_l integer reactor length
|
||||
---@field reactor_w integer reactor width
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
|
||||
-- new core map box
|
||||
---@param args core_map_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function core_map(args)
|
||||
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field")
|
||||
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field")
|
||||
|
||||
-- require max dimensions
|
||||
args.width = 18
|
||||
args.height = 18
|
||||
|
||||
-- inherit only foreground color
|
||||
args.fg_bg = core.graphics.cpair(args.parent.get_fg_bg().fgd, colors.gray)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local alternator = true
|
||||
|
||||
local core_l = args.reactor_l - 2
|
||||
local core_w = args.reactor_w - 2
|
||||
|
||||
local shift_x = 8 - math.floor(core_l / 2)
|
||||
local shift_y = 8 - math.floor(core_w / 2)
|
||||
|
||||
local start_x = 2 + shift_x
|
||||
local start_y = 2 + shift_y
|
||||
|
||||
local inner_width = core_l
|
||||
local inner_height = core_w
|
||||
|
||||
-- create coordinate grid and frame
|
||||
local function draw_frame()
|
||||
e.window.setTextColor(colors.white)
|
||||
|
||||
for x = 0, (inner_width - 1) do
|
||||
e.window.setCursorPos(x + start_x, 1)
|
||||
e.window.write(util.sprintf("%X", x))
|
||||
end
|
||||
|
||||
for y = 0, (inner_height - 1) do
|
||||
e.window.setCursorPos(1, y + start_y)
|
||||
e.window.write(util.sprintf("%X", y))
|
||||
end
|
||||
|
||||
-- even out bottom edge
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg)
|
||||
e.window.setCursorPos(1, e.frame.h)
|
||||
e.window.write(util.strrep("\x8f", e.frame.w))
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- draw the core
|
||||
---@param t number temperature in K
|
||||
local function draw_core(t)
|
||||
local i = 1
|
||||
local back_c = "F"
|
||||
local text_c = "8"
|
||||
|
||||
-- determine fuel assembly coloring
|
||||
if t <= 300 then
|
||||
-- gray
|
||||
text_c = "8"
|
||||
elseif t <= 350 then
|
||||
-- blue
|
||||
text_c = "3"
|
||||
elseif t < 600 then
|
||||
-- green
|
||||
text_c = "D"
|
||||
elseif t < 1000 then
|
||||
-- yellow
|
||||
text_c = "4"
|
||||
-- back_c = "8"
|
||||
elseif t < 1200 then
|
||||
-- orange
|
||||
text_c = "1"
|
||||
elseif t < 1300 then
|
||||
-- red
|
||||
text_c = "E"
|
||||
else
|
||||
-- pink
|
||||
text_c = "2"
|
||||
end
|
||||
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
i = i + 1
|
||||
e.window.blit("\x07", text_c, back_c)
|
||||
else
|
||||
e.window.blit("\x07", "7", "8")
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param temperature number temperature in Kelvin
|
||||
function e.on_update(temperature)
|
||||
e.value = temperature
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- set temperature to display
|
||||
---@param val number degrees K
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- resize reactor dimensions
|
||||
---@param reactor_l integer reactor length (rendered in 2D top-down as width)
|
||||
---@param reactor_w integer reactor width (rendered in 2D top-down as height)
|
||||
function e.resize(reactor_l, reactor_w)
|
||||
-- enforce possible dimensions
|
||||
if reactor_l > 18 then reactor_l = 18 elseif reactor_l < 3 then reactor_l = 3 end
|
||||
if reactor_w > 18 then reactor_w = 18 elseif reactor_w < 3 then reactor_w = 3 end
|
||||
|
||||
-- update dimensions
|
||||
core_l = reactor_l - 2
|
||||
core_w = reactor_w - 2
|
||||
shift_x = 8 - math.floor(core_l / 2)
|
||||
shift_y = 8 - math.floor(core_w / 2)
|
||||
start_x = 2 + shift_x
|
||||
start_y = 2 + shift_y
|
||||
inner_width = core_l
|
||||
inner_height = core_w
|
||||
|
||||
e.window.clear()
|
||||
|
||||
-- re-draw
|
||||
draw_frame()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial (one-time except for resize()) frame draw
|
||||
draw_frame()
|
||||
|
||||
-- initial draw
|
||||
e.on_update(0)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return core_map
|
||||
105
graphics/elements/indicators/data.lua
Normal file
105
graphics/elements/indicators/data.lua
Normal file
@@ -0,0 +1,105 @@
|
||||
-- Data Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
-- format a number string with commas as the thousands separator
|
||||
--
|
||||
-- subtracts from spaces at the start if present for each comma used
|
||||
---@param num string number string
|
||||
---@return string
|
||||
local function comma_format(num)
|
||||
local formatted = num
|
||||
local commas = 0
|
||||
local i = 1
|
||||
|
||||
while i > 0 do
|
||||
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2')
|
||||
if i > 0 then commas = commas + 1 end
|
||||
end
|
||||
|
||||
local _, num_spaces = formatted:gsub(" %s-", "")
|
||||
local remove = math.min(num_spaces, commas)
|
||||
|
||||
formatted = string.sub(formatted, remove + 1)
|
||||
|
||||
return formatted
|
||||
end
|
||||
|
||||
---@class data_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field unit? string indicator unit
|
||||
---@field format string data format (lua string format)
|
||||
---@field commas boolean whether to use commas if a number is given (default to false)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new data indicator
|
||||
---@param args data_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function data(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field")
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
local data_start = string.len(args.label) + 2
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
function e.on_update(value)
|
||||
e.value = value
|
||||
|
||||
local data_str = util.sprintf(args.format, value)
|
||||
|
||||
-- write data
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
if args.commas then
|
||||
e.window.write(comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
end
|
||||
|
||||
-- write label
|
||||
if args.unit ~= nil then
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. args.unit)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return data
|
||||
123
graphics/elements/indicators/hbar.lua
Normal file
123
graphics/elements/indicators/hbar.lua
Normal file
@@ -0,0 +1,123 @@
|
||||
-- Horizontal Bar Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class hbar_args
|
||||
---@field show_percent? boolean whether or not to show the percent
|
||||
---@field bar_fg_bg? cpair bar foreground/background colors if showing percent
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new horizontal bar
|
||||
---@param args hbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
---@return graphics_element element, element_id id
|
||||
local function hbar(args)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- bar width is width - 5 characters for " 100%" if showing percent
|
||||
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
|
||||
|
||||
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar")
|
||||
|
||||
-- determine bar colors
|
||||
local bar_bkg = e.fg_bg.blit_bkg
|
||||
local bar_fgd = e.fg_bg.blit_fgd
|
||||
if args.bar_fg_bg ~= nil then
|
||||
bar_bkg = args.bar_fg_bg.blit_bkg
|
||||
bar_fgd = args.bar_fg_bg.blit_fgd
|
||||
end
|
||||
|
||||
-- handle data changes
|
||||
---@param fraction number 0.0 to 1.0
|
||||
function e.on_update(fraction)
|
||||
e.value = fraction
|
||||
|
||||
-- enforce minimum and maximum
|
||||
if fraction < 0 then
|
||||
fraction = 0.0
|
||||
elseif fraction > 1 then
|
||||
fraction = 1.0
|
||||
end
|
||||
|
||||
-- compute number of bars
|
||||
local num_bars = util.round(fraction * (bar_width * 2))
|
||||
|
||||
-- redraw bar if changed
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
local fgd = ""
|
||||
local bkg = ""
|
||||
local spaces = ""
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 2 do
|
||||
spaces = spaces .. " "
|
||||
fgd = fgd .. bar_fgd
|
||||
bkg = bkg .. bar_bkg
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 2 == 1 then
|
||||
spaces = spaces .. "\x95"
|
||||
fgd = fgd .. bar_bkg
|
||||
bkg = bkg .. bar_fgd
|
||||
end
|
||||
|
||||
-- pad background
|
||||
for _ = 1, ((bar_width * 2) - num_bars) / 2 do
|
||||
spaces = spaces .. " "
|
||||
fgd = fgd .. bar_bkg
|
||||
bkg = bkg .. bar_bkg
|
||||
end
|
||||
|
||||
-- draw bar
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
end
|
||||
end
|
||||
|
||||
-- update percentage
|
||||
if args.show_percent then
|
||||
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.window.write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
end
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param bar_fg_bg cpair new bar colors
|
||||
function e.recolor(bar_fg_bg)
|
||||
bar_bkg = bar_fg_bg.blit_bkg
|
||||
bar_fgd = bar_fg_bg.blit_fgd
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initialize to 0
|
||||
e.on_update(0)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return hbar
|
||||
73
graphics/elements/indicators/icon.lua
Normal file
73
graphics/elements/indicators/icon.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
-- Icon Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class icon_sym_color
|
||||
---@field color cpair
|
||||
---@field symbol string
|
||||
|
||||
---@class icon_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field states table state color and symbol table
|
||||
---@field value? integer default state, defaults to 1
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new icon indicator
|
||||
---@param args icon_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function icon(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field")
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
for i = 1, #args.states do
|
||||
local sym_color = args.states[i] ---@type icon_sym_color
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = " " .. sym_color.symbol .. " ",
|
||||
fgd = util.strrep(sym_color.color.blit_fgd, 3),
|
||||
bkg = util.strrep(sym_color.color.blit_bkg, 3)
|
||||
})
|
||||
end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.window.setCursorPos(5, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial icon draw
|
||||
e.on_update(args.value or 1)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return icon
|
||||
54
graphics/elements/indicators/light.lua
Normal file
54
graphics/elements/indicators/light.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
-- Indicator Light Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class indicator_light_args
|
||||
---@field label string indicator label
|
||||
---@field colors cpair on/off colors (a/b respectively)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new indicator light
|
||||
---@param args indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- on state change
|
||||
---@param new_state boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
if new_state then
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return indicator_light
|
||||
79
graphics/elements/indicators/state.lua
Normal file
79
graphics/elements/indicators/state.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
-- State (Text) Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class state_text_color
|
||||
---@field color cpair
|
||||
---@field text string
|
||||
|
||||
---@class state_indicator_args
|
||||
---@field states table state color and text table
|
||||
---@field value? integer default state, defaults to 1
|
||||
---@field min_width? integer max state text length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field height? integer 1 if omitted, must be an odd number
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new state indicator
|
||||
---@param args state_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function state_indicator(args)
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field")
|
||||
|
||||
-- determine height
|
||||
if util.is_int(args.height) then
|
||||
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number")
|
||||
else
|
||||
args.height = 1
|
||||
end
|
||||
|
||||
-- initial guess at width
|
||||
args.width = args.min_width or 1
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
for i = 1, #args.states do
|
||||
local state_def = args.states[i] ---@type state_text_color
|
||||
|
||||
-- re-determine width
|
||||
if string.len(state_def.text) > args.width then
|
||||
args.width = string.len(state_def.text)
|
||||
end
|
||||
|
||||
local text = util.pad(state_def.text, args.width)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = text,
|
||||
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = util.strrep(state_def.color.blit_bkg, string.len(text))
|
||||
})
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(args.value or 1)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return state_indicator
|
||||
65
graphics/elements/indicators/trilight.lua
Normal file
65
graphics/elements/indicators/trilight.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- Tri-State Indicator Light Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class tristate_indicator_light_args
|
||||
---@field label string indicator label
|
||||
---@field c1 color color for state 1
|
||||
---@field c2 color color for state 2
|
||||
---@field c3 color color for state 3
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new indicator light
|
||||
---@param args tristate_indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tristate_indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- blit translations
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
if new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(0)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return tristate_indicator_light
|
||||
102
graphics/elements/indicators/vbar.lua
Normal file
102
graphics/elements/indicators/vbar.lua
Normal file
@@ -0,0 +1,102 @@
|
||||
-- Vertical Bar Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class vbar_args
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new vertical bar
|
||||
---@param args vbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function vbar(args)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- blit strings
|
||||
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local one_third = util.strrep("\x8f", e.frame.w)
|
||||
local two_thirds = util.strrep("\x83", e.frame.w)
|
||||
|
||||
-- handle data changes
|
||||
---@param fraction number 0.0 to 1.0
|
||||
function e.on_update(fraction)
|
||||
e.value = fraction
|
||||
|
||||
-- enforce minimum and maximum
|
||||
if fraction < 0 then
|
||||
fraction = 0.0
|
||||
elseif fraction > 1 then
|
||||
fraction = 1.0
|
||||
end
|
||||
|
||||
-- compute number of bars
|
||||
local num_bars = util.round(fraction * (e.frame.h * 3))
|
||||
|
||||
-- redraw only if number of bars has changed
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
-- start bottom up
|
||||
local y = e.frame.h
|
||||
|
||||
-- start at base of vertical bar
|
||||
e.window.setCursorPos(1, y)
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 3 do
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
y = y - 1
|
||||
e.window.setCursorPos(1, y)
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 3 == 1 then
|
||||
e.window.blit(one_third, bkg, fgd)
|
||||
y = y - 1
|
||||
elseif num_bars % 3 == 2 then
|
||||
e.window.blit(two_thirds, bkg, fgd)
|
||||
y = y - 1
|
||||
end
|
||||
|
||||
-- fill the rest blank
|
||||
while y > 0 do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.window.blit(spaces, fgd, bkg)
|
||||
y = y - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return vbar
|
||||
147
graphics/elements/pipenet.lua
Normal file
147
graphics/elements/pipenet.lua
Normal file
@@ -0,0 +1,147 @@
|
||||
-- Pipe Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class pipenet_args
|
||||
---@field pipes table pipe list
|
||||
---@field bg? color background color
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
|
||||
-- new pipe network
|
||||
---@param args pipenet_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function pipenet(args)
|
||||
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field")
|
||||
|
||||
args.width = 0
|
||||
args.height = 0
|
||||
|
||||
-- determine width/height
|
||||
for i = 1, #args.pipes do
|
||||
local pipe = args.pipes[i] ---@type pipe
|
||||
|
||||
local true_w = pipe.w + math.min(pipe.x1, pipe.x2)
|
||||
local true_h = pipe.h + math.min(pipe.y1, pipe.y2)
|
||||
|
||||
if true_w > args.width then args.width = true_w end
|
||||
if true_h > args.height then args.height = true_h end
|
||||
end
|
||||
|
||||
args.x = args.x or 1
|
||||
args.y = args.y or 1
|
||||
|
||||
if args.bg ~= nil then
|
||||
args.fg_bg = core.graphics.cpair(args.bg, args.bg)
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw all pipes
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
e.window.setCursorPos(x, y)
|
||||
|
||||
local c = core.graphics.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.window.blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return pipenet
|
||||
116
graphics/elements/rectangle.lua
Normal file
116
graphics/elements/rectangle.lua
Normal file
@@ -0,0 +1,116 @@
|
||||
-- Rectangle Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class rectangle_args
|
||||
---@field border? graphics_border
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new rectangle
|
||||
---@param args rectangle_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rectangle(args)
|
||||
-- offset children
|
||||
if args.border ~= nil then
|
||||
args.offset_x = args.border.width
|
||||
args.offset_y = args.border.width
|
||||
|
||||
-- slightly different y offset if the border is set to even
|
||||
if args.border.even then
|
||||
local width_x2 = (2 * args.border.width)
|
||||
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw bordered box if requested
|
||||
-- element constructor will have drawn basic colored rectangle regardless
|
||||
if args.border ~= nil then
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
local border_width = args.offset_x
|
||||
local border_height = args.offset_y
|
||||
local border_blit = colors.toBlit(args.border.color)
|
||||
local width_x2 = border_width * 2
|
||||
local inner_width = e.frame.w - width_x2
|
||||
|
||||
-- check dimensions
|
||||
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width")
|
||||
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height")
|
||||
|
||||
-- form the basic line strings and top/bottom blit strings
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_bg_sides = ""
|
||||
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
|
||||
|
||||
-- partial bars
|
||||
local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
|
||||
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
util.strrep(border_blit, border_width)
|
||||
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) ..
|
||||
util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
-- form the body blit strings (sides are border, inside is normal)
|
||||
for x = 1, e.frame.w do
|
||||
-- edges get border color, center gets normal
|
||||
if x <= border_width or x > (e.frame.w - border_width) then
|
||||
blit_bg_sides = blit_bg_sides .. border_blit
|
||||
else
|
||||
blit_bg_sides = blit_bg_sides .. e.fg_bg.blit_bkg
|
||||
end
|
||||
end
|
||||
|
||||
-- draw rectangle with borders
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_b, p_inv_bg, p_inv_fg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_a, p_inv_fg, blit_bg_top_bot)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_b, p_inv_fg, blit_bg_top_bot)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return rectangle
|
||||
70
graphics/elements/textbox.lua
Normal file
70
graphics/elements/textbox.lua
Normal file
@@ -0,0 +1,70 @@
|
||||
-- Text Box Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
---@class textbox_args
|
||||
---@field text string text to show
|
||||
---@field alignment? TEXT_ALIGN text alignment, left by default
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new text box
|
||||
---@param args textbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function textbox(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local alignment = args.alignment or TEXT_ALIGN.LEFT
|
||||
|
||||
-- draw textbox
|
||||
|
||||
local function display_text(text)
|
||||
e.value = text
|
||||
|
||||
local lines = util.strwrap(text, e.frame.w)
|
||||
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
if alignment == TEXT_ALIGN.CENTER then
|
||||
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i)
|
||||
elseif alignment == TEXT_ALIGN.RIGHT then
|
||||
e.window.setCursorPos((e.frame.w - len) + 1, i)
|
||||
else
|
||||
e.window.setCursorPos(1, i)
|
||||
end
|
||||
|
||||
e.window.write(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
display_text(args.text)
|
||||
|
||||
-- set the string value and re-draw the text
|
||||
---@param val string value
|
||||
function e.set_value(val)
|
||||
e.window.clear()
|
||||
display_text(val)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return textbox
|
||||
87
graphics/elements/tiling.lua
Normal file
87
graphics/elements/tiling.lua
Normal file
@@ -0,0 +1,87 @@
|
||||
-- "Basketweave" Tiling Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class tiling_args
|
||||
---@field fill_c cpair colors to fill with
|
||||
---@field even? boolean whether to account for rectangular pixels
|
||||
---@field border_c? color optional frame color
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new tiling box
|
||||
---@param args tiling_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tiling(args)
|
||||
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw tiling box
|
||||
|
||||
local fill_a = args.fill_c.blit_a
|
||||
local fill_b = args.fill_c.blit_b
|
||||
|
||||
local even = args.even == true
|
||||
|
||||
local start_x = 1
|
||||
local start_y = 1
|
||||
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
|
||||
local inner_height = e.frame.h
|
||||
local alternator = true
|
||||
|
||||
-- border
|
||||
if args.border_c ~= nil then
|
||||
e.window.setBackgroundColor(args.border_c)
|
||||
e.window.clear()
|
||||
|
||||
start_x = 1 + util.trinary(even, 2, 1)
|
||||
start_y = 2
|
||||
|
||||
inner_width = math.floor((e.frame.w - 2 * util.trinary(even, 2, 1)) / util.trinary(even, 2, 1))
|
||||
inner_height = e.frame.h - 2
|
||||
end
|
||||
|
||||
-- check dimensions
|
||||
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0")
|
||||
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0")
|
||||
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width")
|
||||
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height")
|
||||
|
||||
-- create pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
for x = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return tiling
|
||||
18
initenv.lua
Normal file
18
initenv.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
--
|
||||
-- Initialize the Post-Boot Module Environment
|
||||
--
|
||||
|
||||
return {
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require = require("cc.require")
|
||||
local _env = setmetatable({}, { __index = _ENV })
|
||||
|
||||
-- overwrite require/package globals
|
||||
require, package = _require.make(_env, "/")
|
||||
|
||||
-- reset terminal
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
}
|
||||
22
lockbox/LICENSE
Normal file
22
lockbox/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 James L.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
415
lockbox/cipher/aes128.lua
Normal file
415
lockbox/cipher/aes128.lua
Normal file
@@ -0,0 +1,415 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local SBOX = {
|
||||
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
|
||||
|
||||
local ISBOX = {
|
||||
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
|
||||
|
||||
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
|
||||
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
|
||||
|
||||
local ETABLE = {
|
||||
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
|
||||
|
||||
local LTABLE = {
|
||||
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
|
||||
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
|
||||
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
|
||||
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
|
||||
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
|
||||
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
||||
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
|
||||
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
|
||||
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
|
||||
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
|
||||
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
|
||||
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
|
||||
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
|
||||
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
|
||||
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
|
||||
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
|
||||
|
||||
local MIXTABLE = {
|
||||
0x02, 0x03, 0x01, 0x01,
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
0x01, 0x01, 0x02, 0x03,
|
||||
0x03, 0x01, 0x01, 0x02};
|
||||
|
||||
local IMIXTABLE = {
|
||||
0x0E, 0x0B, 0x0D, 0x09,
|
||||
0x09, 0x0E, 0x0B, 0x0D,
|
||||
0x0D, 0x09, 0x0E, 0x0B,
|
||||
0x0B, 0x0D, 0x09, 0x0E};
|
||||
|
||||
local RCON = {
|
||||
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
|
||||
|
||||
|
||||
local GMUL = function(A, B)
|
||||
if(A == 0x01) then return B; end
|
||||
if(B == 0x01) then return A; end
|
||||
if(A == 0x00) then return 0; end
|
||||
if(B == 0x00) then return 0; end
|
||||
|
||||
local LA = LTABLE[A];
|
||||
local LB = LTABLE[B];
|
||||
|
||||
local sum = LA + LB;
|
||||
if (sum > 0xFF) then sum = sum - 0xFF; end
|
||||
|
||||
return ETABLE[sum];
|
||||
end
|
||||
|
||||
local byteSub = Array.substitute;
|
||||
|
||||
local shiftRow = Array.permute;
|
||||
|
||||
local mixCol = function(i, mix)
|
||||
local out = {};
|
||||
|
||||
local a, b, c, d;
|
||||
|
||||
a = GMUL(i[ 1], mix[ 1]);
|
||||
b = GMUL(i[ 2], mix[ 2]);
|
||||
c = GMUL(i[ 3], mix[ 3]);
|
||||
d = GMUL(i[ 4], mix[ 4]);
|
||||
out[ 1] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 5]);
|
||||
b = GMUL(i[ 2], mix[ 6]);
|
||||
c = GMUL(i[ 3], mix[ 7]);
|
||||
d = GMUL(i[ 4], mix[ 8]);
|
||||
out[ 2] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 9]);
|
||||
b = GMUL(i[ 2], mix[10]);
|
||||
c = GMUL(i[ 3], mix[11]);
|
||||
d = GMUL(i[ 4], mix[12]);
|
||||
out[ 3] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[13]);
|
||||
b = GMUL(i[ 2], mix[14]);
|
||||
c = GMUL(i[ 3], mix[15]);
|
||||
d = GMUL(i[ 4], mix[16]);
|
||||
out[ 4] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 5], mix[ 1]);
|
||||
b = GMUL(i[ 6], mix[ 2]);
|
||||
c = GMUL(i[ 7], mix[ 3]);
|
||||
d = GMUL(i[ 8], mix[ 4]);
|
||||
out[ 5] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 5]);
|
||||
b = GMUL(i[ 6], mix[ 6]);
|
||||
c = GMUL(i[ 7], mix[ 7]);
|
||||
d = GMUL(i[ 8], mix[ 8]);
|
||||
out[ 6] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 9]);
|
||||
b = GMUL(i[ 6], mix[10]);
|
||||
c = GMUL(i[ 7], mix[11]);
|
||||
d = GMUL(i[ 8], mix[12]);
|
||||
out[ 7] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[13]);
|
||||
b = GMUL(i[ 6], mix[14]);
|
||||
c = GMUL(i[ 7], mix[15]);
|
||||
d = GMUL(i[ 8], mix[16]);
|
||||
out[ 8] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 9], mix[ 1]);
|
||||
b = GMUL(i[10], mix[ 2]);
|
||||
c = GMUL(i[11], mix[ 3]);
|
||||
d = GMUL(i[12], mix[ 4]);
|
||||
out[ 9] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 5]);
|
||||
b = GMUL(i[10], mix[ 6]);
|
||||
c = GMUL(i[11], mix[ 7]);
|
||||
d = GMUL(i[12], mix[ 8]);
|
||||
out[10] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 9]);
|
||||
b = GMUL(i[10], mix[10]);
|
||||
c = GMUL(i[11], mix[11]);
|
||||
d = GMUL(i[12], mix[12]);
|
||||
out[11] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[13]);
|
||||
b = GMUL(i[10], mix[14]);
|
||||
c = GMUL(i[11], mix[15]);
|
||||
d = GMUL(i[12], mix[16]);
|
||||
out[12] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[13], mix[ 1]);
|
||||
b = GMUL(i[14], mix[ 2]);
|
||||
c = GMUL(i[15], mix[ 3]);
|
||||
d = GMUL(i[16], mix[ 4]);
|
||||
out[13] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 5]);
|
||||
b = GMUL(i[14], mix[ 6]);
|
||||
c = GMUL(i[15], mix[ 7]);
|
||||
d = GMUL(i[16], mix[ 8]);
|
||||
out[14] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 9]);
|
||||
b = GMUL(i[14], mix[10]);
|
||||
c = GMUL(i[15], mix[11]);
|
||||
d = GMUL(i[16], mix[12]);
|
||||
out[15] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[13]);
|
||||
b = GMUL(i[14], mix[14]);
|
||||
c = GMUL(i[15], mix[15]);
|
||||
d = GMUL(i[16], mix[16]);
|
||||
out[16] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyRound = function(key, round)
|
||||
local out = {};
|
||||
|
||||
out[ 1] = XOR(key[ 1], XOR(SBOX[key[14]], RCON[round]));
|
||||
out[ 2] = XOR(key[ 2], SBOX[key[15]]);
|
||||
out[ 3] = XOR(key[ 3], SBOX[key[16]]);
|
||||
out[ 4] = XOR(key[ 4], SBOX[key[13]]);
|
||||
|
||||
out[ 5] = XOR(out[ 1], key[ 5]);
|
||||
out[ 6] = XOR(out[ 2], key[ 6]);
|
||||
out[ 7] = XOR(out[ 3], key[ 7]);
|
||||
out[ 8] = XOR(out[ 4], key[ 8]);
|
||||
|
||||
out[ 9] = XOR(out[ 5], key[ 9]);
|
||||
out[10] = XOR(out[ 6], key[10]);
|
||||
out[11] = XOR(out[ 7], key[11]);
|
||||
out[12] = XOR(out[ 8], key[12]);
|
||||
|
||||
out[13] = XOR(out[ 9], key[13]);
|
||||
out[14] = XOR(out[10], key[14]);
|
||||
out[15] = XOR(out[11], key[15]);
|
||||
out[16] = XOR(out[12], key[16]);
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyExpand = function(key)
|
||||
local keys = {};
|
||||
|
||||
local temp = key;
|
||||
|
||||
keys[1] = temp;
|
||||
|
||||
for i = 1, 10 do
|
||||
temp = keyRound(temp, i);
|
||||
keys[i + 1] = temp;
|
||||
end
|
||||
|
||||
return keys;
|
||||
|
||||
end
|
||||
|
||||
local addKey = Array.XOR;
|
||||
|
||||
|
||||
|
||||
local AES = {};
|
||||
|
||||
AES.blockSize = 16;
|
||||
|
||||
AES.encrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
--round 1
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[2]);
|
||||
|
||||
--round 2
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[3]);
|
||||
|
||||
--round 3
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[4]);
|
||||
|
||||
--round 4
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[5]);
|
||||
|
||||
--round 5
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[6]);
|
||||
|
||||
--round 6
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[7]);
|
||||
|
||||
--round 7
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[8]);
|
||||
|
||||
--round 8
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[9]);
|
||||
|
||||
--round 9
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[10]);
|
||||
|
||||
--round 10
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
return block;
|
||||
|
||||
end
|
||||
|
||||
AES.decrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
--round 1
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[10]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 2
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[9]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 3
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[8]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 4
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[7]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 5
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[6]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 6
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[5]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 7
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[4]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 8
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[3]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 9
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[2]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 10
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
return block;
|
||||
end
|
||||
|
||||
return AES;
|
||||
462
lockbox/cipher/aes192.lua
Normal file
462
lockbox/cipher/aes192.lua
Normal file
@@ -0,0 +1,462 @@
|
||||
|
||||
local Array = require("lockbox.util.array");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local SBOX = {
|
||||
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
|
||||
|
||||
local ISBOX = {
|
||||
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
|
||||
|
||||
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
|
||||
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
|
||||
|
||||
local ETABLE = {
|
||||
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
|
||||
|
||||
local LTABLE = {
|
||||
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
|
||||
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
|
||||
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
|
||||
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
|
||||
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
|
||||
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
||||
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
|
||||
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
|
||||
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
|
||||
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
|
||||
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
|
||||
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
|
||||
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
|
||||
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
|
||||
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
|
||||
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
|
||||
|
||||
local MIXTABLE = {
|
||||
0x02, 0x03, 0x01, 0x01,
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
0x01, 0x01, 0x02, 0x03,
|
||||
0x03, 0x01, 0x01, 0x02};
|
||||
|
||||
local IMIXTABLE = {
|
||||
0x0E, 0x0B, 0x0D, 0x09,
|
||||
0x09, 0x0E, 0x0B, 0x0D,
|
||||
0x0D, 0x09, 0x0E, 0x0B,
|
||||
0x0B, 0x0D, 0x09, 0x0E};
|
||||
|
||||
local RCON = {
|
||||
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
|
||||
|
||||
|
||||
local GMUL = function(A, B)
|
||||
if(A == 0x01) then return B; end
|
||||
if(B == 0x01) then return A; end
|
||||
if(A == 0x00) then return 0; end
|
||||
if(B == 0x00) then return 0; end
|
||||
|
||||
local LA = LTABLE[A];
|
||||
local LB = LTABLE[B];
|
||||
|
||||
local sum = LA + LB;
|
||||
if (sum > 0xFF) then sum = sum - 0xFF; end
|
||||
|
||||
return ETABLE[sum];
|
||||
end
|
||||
|
||||
local byteSub = Array.substitute;
|
||||
|
||||
local shiftRow = Array.permute;
|
||||
|
||||
local mixCol = function(i, mix)
|
||||
local out = {};
|
||||
|
||||
local a, b, c, d;
|
||||
|
||||
a = GMUL(i[ 1], mix[ 1]);
|
||||
b = GMUL(i[ 2], mix[ 2]);
|
||||
c = GMUL(i[ 3], mix[ 3]);
|
||||
d = GMUL(i[ 4], mix[ 4]);
|
||||
out[ 1] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 5]);
|
||||
b = GMUL(i[ 2], mix[ 6]);
|
||||
c = GMUL(i[ 3], mix[ 7]);
|
||||
d = GMUL(i[ 4], mix[ 8]);
|
||||
out[ 2] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 9]);
|
||||
b = GMUL(i[ 2], mix[10]);
|
||||
c = GMUL(i[ 3], mix[11]);
|
||||
d = GMUL(i[ 4], mix[12]);
|
||||
out[ 3] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[13]);
|
||||
b = GMUL(i[ 2], mix[14]);
|
||||
c = GMUL(i[ 3], mix[15]);
|
||||
d = GMUL(i[ 4], mix[16]);
|
||||
out[ 4] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 5], mix[ 1]);
|
||||
b = GMUL(i[ 6], mix[ 2]);
|
||||
c = GMUL(i[ 7], mix[ 3]);
|
||||
d = GMUL(i[ 8], mix[ 4]);
|
||||
out[ 5] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 5]);
|
||||
b = GMUL(i[ 6], mix[ 6]);
|
||||
c = GMUL(i[ 7], mix[ 7]);
|
||||
d = GMUL(i[ 8], mix[ 8]);
|
||||
out[ 6] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 9]);
|
||||
b = GMUL(i[ 6], mix[10]);
|
||||
c = GMUL(i[ 7], mix[11]);
|
||||
d = GMUL(i[ 8], mix[12]);
|
||||
out[ 7] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[13]);
|
||||
b = GMUL(i[ 6], mix[14]);
|
||||
c = GMUL(i[ 7], mix[15]);
|
||||
d = GMUL(i[ 8], mix[16]);
|
||||
out[ 8] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 9], mix[ 1]);
|
||||
b = GMUL(i[10], mix[ 2]);
|
||||
c = GMUL(i[11], mix[ 3]);
|
||||
d = GMUL(i[12], mix[ 4]);
|
||||
out[ 9] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 5]);
|
||||
b = GMUL(i[10], mix[ 6]);
|
||||
c = GMUL(i[11], mix[ 7]);
|
||||
d = GMUL(i[12], mix[ 8]);
|
||||
out[10] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 9]);
|
||||
b = GMUL(i[10], mix[10]);
|
||||
c = GMUL(i[11], mix[11]);
|
||||
d = GMUL(i[12], mix[12]);
|
||||
out[11] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[13]);
|
||||
b = GMUL(i[10], mix[14]);
|
||||
c = GMUL(i[11], mix[15]);
|
||||
d = GMUL(i[12], mix[16]);
|
||||
out[12] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[13], mix[ 1]);
|
||||
b = GMUL(i[14], mix[ 2]);
|
||||
c = GMUL(i[15], mix[ 3]);
|
||||
d = GMUL(i[16], mix[ 4]);
|
||||
out[13] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 5]);
|
||||
b = GMUL(i[14], mix[ 6]);
|
||||
c = GMUL(i[15], mix[ 7]);
|
||||
d = GMUL(i[16], mix[ 8]);
|
||||
out[14] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 9]);
|
||||
b = GMUL(i[14], mix[10]);
|
||||
c = GMUL(i[15], mix[11]);
|
||||
d = GMUL(i[16], mix[12]);
|
||||
out[15] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[13]);
|
||||
b = GMUL(i[14], mix[14]);
|
||||
c = GMUL(i[15], mix[15]);
|
||||
d = GMUL(i[16], mix[16]);
|
||||
out[16] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyRound = function(key, round)
|
||||
local i = (round - 1) * 24;
|
||||
local out = key;
|
||||
|
||||
out[25 + i] = XOR(key[ 1 + i], XOR(SBOX[key[22 + i]], RCON[round]));
|
||||
out[26 + i] = XOR(key[ 2 + i], SBOX[key[23 + i]]);
|
||||
out[27 + i] = XOR(key[ 3 + i], SBOX[key[24 + i]]);
|
||||
out[28 + i] = XOR(key[ 4 + i], SBOX[key[21 + i]]);
|
||||
|
||||
out[29 + i] = XOR(out[25 + i], key[ 5 + i]);
|
||||
out[30 + i] = XOR(out[26 + i], key[ 6 + i]);
|
||||
out[31 + i] = XOR(out[27 + i], key[ 7 + i]);
|
||||
out[32 + i] = XOR(out[28 + i], key[ 8 + i]);
|
||||
|
||||
out[33 + i] = XOR(out[29 + i], key[ 9 + i]);
|
||||
out[34 + i] = XOR(out[30 + i], key[10 + i]);
|
||||
out[35 + i] = XOR(out[31 + i], key[11 + i]);
|
||||
out[36 + i] = XOR(out[32 + i], key[12 + i]);
|
||||
|
||||
out[37 + i] = XOR(out[33 + i], key[13 + i]);
|
||||
out[38 + i] = XOR(out[34 + i], key[14 + i]);
|
||||
out[39 + i] = XOR(out[35 + i], key[15 + i]);
|
||||
out[40 + i] = XOR(out[36 + i], key[16 + i]);
|
||||
|
||||
out[41 + i] = XOR(out[37 + i], key[17 + i]);
|
||||
out[42 + i] = XOR(out[38 + i], key[18 + i]);
|
||||
out[43 + i] = XOR(out[39 + i], key[19 + i]);
|
||||
out[44 + i] = XOR(out[40 + i], key[20 + i]);
|
||||
|
||||
out[45 + i] = XOR(out[41 + i], key[21 + i]);
|
||||
out[46 + i] = XOR(out[42 + i], key[22 + i]);
|
||||
out[47 + i] = XOR(out[43 + i], key[23 + i]);
|
||||
out[48 + i] = XOR(out[44 + i], key[24 + i]);
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyExpand = function(key)
|
||||
local bytes = Array.copy(key);
|
||||
|
||||
for i = 1, 8 do
|
||||
keyRound(bytes, i);
|
||||
end
|
||||
|
||||
local keys = {};
|
||||
|
||||
keys[ 1] = Array.slice(bytes, 1, 16);
|
||||
keys[ 2] = Array.slice(bytes, 17, 32);
|
||||
keys[ 3] = Array.slice(bytes, 33, 48);
|
||||
keys[ 4] = Array.slice(bytes, 49, 64);
|
||||
keys[ 5] = Array.slice(bytes, 65, 80);
|
||||
keys[ 6] = Array.slice(bytes, 81, 96);
|
||||
keys[ 7] = Array.slice(bytes, 97, 112);
|
||||
keys[ 8] = Array.slice(bytes, 113, 128);
|
||||
keys[ 9] = Array.slice(bytes, 129, 144);
|
||||
keys[10] = Array.slice(bytes, 145, 160);
|
||||
keys[11] = Array.slice(bytes, 161, 176);
|
||||
keys[12] = Array.slice(bytes, 177, 192);
|
||||
keys[13] = Array.slice(bytes, 193, 208);
|
||||
|
||||
return keys;
|
||||
|
||||
end
|
||||
|
||||
local addKey = Array.XOR;
|
||||
|
||||
|
||||
|
||||
local AES = {};
|
||||
|
||||
AES.blockSize = 16;
|
||||
|
||||
AES.encrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
--round 1
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[2]);
|
||||
|
||||
--round 2
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[3]);
|
||||
|
||||
--round 3
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[4]);
|
||||
|
||||
--round 4
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[5]);
|
||||
|
||||
--round 5
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[6]);
|
||||
|
||||
--round 6
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[7]);
|
||||
|
||||
--round 7
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[8]);
|
||||
|
||||
--round 8
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[9]);
|
||||
|
||||
--round 9
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[10]);
|
||||
|
||||
--round 10
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
--round 11
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[12]);
|
||||
|
||||
--round 12
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = addKey(block, key[13]);
|
||||
|
||||
return block;
|
||||
|
||||
end
|
||||
|
||||
AES.decrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[13]);
|
||||
|
||||
--round 1
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[12]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 2
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[11]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 3
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[10]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 4
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[9]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 5
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[8]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 6
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[7]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 7
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[6]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 8
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[5]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 9
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[4]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 10
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[3]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 11
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[2]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 12
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
return block;
|
||||
end
|
||||
|
||||
return AES;
|
||||
498
lockbox/cipher/aes256.lua
Normal file
498
lockbox/cipher/aes256.lua
Normal file
@@ -0,0 +1,498 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local SBOX = {
|
||||
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
|
||||
|
||||
local ISBOX = {
|
||||
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
|
||||
|
||||
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
|
||||
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
|
||||
|
||||
local ETABLE = {
|
||||
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
|
||||
|
||||
local LTABLE = {
|
||||
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
|
||||
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
|
||||
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
|
||||
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
|
||||
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
|
||||
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
||||
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
|
||||
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
|
||||
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
|
||||
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
|
||||
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
|
||||
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
|
||||
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
|
||||
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
|
||||
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
|
||||
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
|
||||
|
||||
local MIXTABLE = {
|
||||
0x02, 0x03, 0x01, 0x01,
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
0x01, 0x01, 0x02, 0x03,
|
||||
0x03, 0x01, 0x01, 0x02};
|
||||
|
||||
local IMIXTABLE = {
|
||||
0x0E, 0x0B, 0x0D, 0x09,
|
||||
0x09, 0x0E, 0x0B, 0x0D,
|
||||
0x0D, 0x09, 0x0E, 0x0B,
|
||||
0x0B, 0x0D, 0x09, 0x0E};
|
||||
|
||||
local RCON = {
|
||||
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
|
||||
|
||||
|
||||
local GMUL = function(A, B)
|
||||
if(A == 0x01) then return B; end
|
||||
if(B == 0x01) then return A; end
|
||||
if(A == 0x00) then return 0; end
|
||||
if(B == 0x00) then return 0; end
|
||||
|
||||
local LA = LTABLE[A];
|
||||
local LB = LTABLE[B];
|
||||
|
||||
local sum = LA + LB;
|
||||
if (sum > 0xFF) then sum = sum - 0xFF; end
|
||||
|
||||
return ETABLE[sum];
|
||||
end
|
||||
|
||||
local byteSub = Array.substitute;
|
||||
|
||||
local shiftRow = Array.permute;
|
||||
|
||||
local mixCol = function(i, mix)
|
||||
local out = {};
|
||||
|
||||
local a, b, c, d;
|
||||
|
||||
a = GMUL(i[ 1], mix[ 1]);
|
||||
b = GMUL(i[ 2], mix[ 2]);
|
||||
c = GMUL(i[ 3], mix[ 3]);
|
||||
d = GMUL(i[ 4], mix[ 4]);
|
||||
out[ 1] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 5]);
|
||||
b = GMUL(i[ 2], mix[ 6]);
|
||||
c = GMUL(i[ 3], mix[ 7]);
|
||||
d = GMUL(i[ 4], mix[ 8]);
|
||||
out[ 2] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 9]);
|
||||
b = GMUL(i[ 2], mix[10]);
|
||||
c = GMUL(i[ 3], mix[11]);
|
||||
d = GMUL(i[ 4], mix[12]);
|
||||
out[ 3] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[13]);
|
||||
b = GMUL(i[ 2], mix[14]);
|
||||
c = GMUL(i[ 3], mix[15]);
|
||||
d = GMUL(i[ 4], mix[16]);
|
||||
out[ 4] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 5], mix[ 1]);
|
||||
b = GMUL(i[ 6], mix[ 2]);
|
||||
c = GMUL(i[ 7], mix[ 3]);
|
||||
d = GMUL(i[ 8], mix[ 4]);
|
||||
out[ 5] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 5]);
|
||||
b = GMUL(i[ 6], mix[ 6]);
|
||||
c = GMUL(i[ 7], mix[ 7]);
|
||||
d = GMUL(i[ 8], mix[ 8]);
|
||||
out[ 6] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 9]);
|
||||
b = GMUL(i[ 6], mix[10]);
|
||||
c = GMUL(i[ 7], mix[11]);
|
||||
d = GMUL(i[ 8], mix[12]);
|
||||
out[ 7] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[13]);
|
||||
b = GMUL(i[ 6], mix[14]);
|
||||
c = GMUL(i[ 7], mix[15]);
|
||||
d = GMUL(i[ 8], mix[16]);
|
||||
out[ 8] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 9], mix[ 1]);
|
||||
b = GMUL(i[10], mix[ 2]);
|
||||
c = GMUL(i[11], mix[ 3]);
|
||||
d = GMUL(i[12], mix[ 4]);
|
||||
out[ 9] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 5]);
|
||||
b = GMUL(i[10], mix[ 6]);
|
||||
c = GMUL(i[11], mix[ 7]);
|
||||
d = GMUL(i[12], mix[ 8]);
|
||||
out[10] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 9]);
|
||||
b = GMUL(i[10], mix[10]);
|
||||
c = GMUL(i[11], mix[11]);
|
||||
d = GMUL(i[12], mix[12]);
|
||||
out[11] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[13]);
|
||||
b = GMUL(i[10], mix[14]);
|
||||
c = GMUL(i[11], mix[15]);
|
||||
d = GMUL(i[12], mix[16]);
|
||||
out[12] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[13], mix[ 1]);
|
||||
b = GMUL(i[14], mix[ 2]);
|
||||
c = GMUL(i[15], mix[ 3]);
|
||||
d = GMUL(i[16], mix[ 4]);
|
||||
out[13] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 5]);
|
||||
b = GMUL(i[14], mix[ 6]);
|
||||
c = GMUL(i[15], mix[ 7]);
|
||||
d = GMUL(i[16], mix[ 8]);
|
||||
out[14] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 9]);
|
||||
b = GMUL(i[14], mix[10]);
|
||||
c = GMUL(i[15], mix[11]);
|
||||
d = GMUL(i[16], mix[12]);
|
||||
out[15] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[13]);
|
||||
b = GMUL(i[14], mix[14]);
|
||||
c = GMUL(i[15], mix[15]);
|
||||
d = GMUL(i[16], mix[16]);
|
||||
out[16] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyRound = function(key, round)
|
||||
local i = (round - 1) * 32;
|
||||
local out = key;
|
||||
|
||||
out[33 + i] = XOR(key[ 1 + i], XOR(SBOX[key[30 + i]], RCON[round]));
|
||||
out[34 + i] = XOR(key[ 2 + i], SBOX[key[31 + i]]);
|
||||
out[35 + i] = XOR(key[ 3 + i], SBOX[key[32 + i]]);
|
||||
out[36 + i] = XOR(key[ 4 + i], SBOX[key[29 + i]]);
|
||||
|
||||
out[37 + i] = XOR(out[33 + i], key[ 5 + i]);
|
||||
out[38 + i] = XOR(out[34 + i], key[ 6 + i]);
|
||||
out[39 + i] = XOR(out[35 + i], key[ 7 + i]);
|
||||
out[40 + i] = XOR(out[36 + i], key[ 8 + i]);
|
||||
|
||||
out[41 + i] = XOR(out[37 + i], key[ 9 + i]);
|
||||
out[42 + i] = XOR(out[38 + i], key[10 + i]);
|
||||
out[43 + i] = XOR(out[39 + i], key[11 + i]);
|
||||
out[44 + i] = XOR(out[40 + i], key[12 + i]);
|
||||
|
||||
out[45 + i] = XOR(out[41 + i], key[13 + i]);
|
||||
out[46 + i] = XOR(out[42 + i], key[14 + i]);
|
||||
out[47 + i] = XOR(out[43 + i], key[15 + i]);
|
||||
out[48 + i] = XOR(out[44 + i], key[16 + i]);
|
||||
|
||||
|
||||
out[49 + i] = XOR(SBOX[out[45 + i]], key[17 + i]);
|
||||
out[50 + i] = XOR(SBOX[out[46 + i]], key[18 + i]);
|
||||
out[51 + i] = XOR(SBOX[out[47 + i]], key[19 + i]);
|
||||
out[52 + i] = XOR(SBOX[out[48 + i]], key[20 + i]);
|
||||
|
||||
out[53 + i] = XOR(out[49 + i], key[21 + i]);
|
||||
out[54 + i] = XOR(out[50 + i], key[22 + i]);
|
||||
out[55 + i] = XOR(out[51 + i], key[23 + i]);
|
||||
out[56 + i] = XOR(out[52 + i], key[24 + i]);
|
||||
|
||||
out[57 + i] = XOR(out[53 + i], key[25 + i]);
|
||||
out[58 + i] = XOR(out[54 + i], key[26 + i]);
|
||||
out[59 + i] = XOR(out[55 + i], key[27 + i]);
|
||||
out[60 + i] = XOR(out[56 + i], key[28 + i]);
|
||||
|
||||
out[61 + i] = XOR(out[57 + i], key[29 + i]);
|
||||
out[62 + i] = XOR(out[58 + i], key[30 + i]);
|
||||
out[63 + i] = XOR(out[59 + i], key[31 + i]);
|
||||
out[64 + i] = XOR(out[60 + i], key[32 + i]);
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyExpand = function(key)
|
||||
local bytes = Array.copy(key);
|
||||
|
||||
for i = 1, 7 do
|
||||
keyRound(bytes, i);
|
||||
end
|
||||
|
||||
local keys = {};
|
||||
|
||||
keys[ 1] = Array.slice(bytes, 1, 16);
|
||||
keys[ 2] = Array.slice(bytes, 17, 32);
|
||||
keys[ 3] = Array.slice(bytes, 33, 48);
|
||||
keys[ 4] = Array.slice(bytes, 49, 64);
|
||||
keys[ 5] = Array.slice(bytes, 65, 80);
|
||||
keys[ 6] = Array.slice(bytes, 81, 96);
|
||||
keys[ 7] = Array.slice(bytes, 97, 112);
|
||||
keys[ 8] = Array.slice(bytes, 113, 128);
|
||||
keys[ 9] = Array.slice(bytes, 129, 144);
|
||||
keys[10] = Array.slice(bytes, 145, 160);
|
||||
keys[11] = Array.slice(bytes, 161, 176);
|
||||
keys[12] = Array.slice(bytes, 177, 192);
|
||||
keys[13] = Array.slice(bytes, 193, 208);
|
||||
keys[14] = Array.slice(bytes, 209, 224);
|
||||
keys[15] = Array.slice(bytes, 225, 240);
|
||||
|
||||
return keys;
|
||||
|
||||
end
|
||||
|
||||
local addKey = Array.XOR;
|
||||
|
||||
|
||||
|
||||
local AES = {};
|
||||
|
||||
AES.blockSize = 16;
|
||||
|
||||
AES.encrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
--round 1
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[2]);
|
||||
|
||||
--round 2
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[3]);
|
||||
|
||||
--round 3
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[4]);
|
||||
|
||||
--round 4
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[5]);
|
||||
|
||||
--round 5
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[6]);
|
||||
|
||||
--round 6
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[7]);
|
||||
|
||||
--round 7
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[8]);
|
||||
|
||||
--round 8
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[9]);
|
||||
|
||||
--round 9
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[10]);
|
||||
|
||||
--round 10
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
--round 11
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[12]);
|
||||
|
||||
--round 12
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[13]);
|
||||
|
||||
--round 13
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[14]);
|
||||
|
||||
--round 14
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = addKey(block, key[15]);
|
||||
|
||||
return block;
|
||||
|
||||
end
|
||||
|
||||
AES.decrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[15]);
|
||||
|
||||
--round 1
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[14]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 2
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[13]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 3
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[12]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 4
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[11]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 5
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[10]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 6
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[9]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 7
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[8]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 8
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[7]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 9
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[6]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 10
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[5]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 11
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[4]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 12
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[3]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 13
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[2]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 14
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
return block;
|
||||
end
|
||||
|
||||
return AES;
|
||||
164
lockbox/cipher/mode/cbc.lua
Normal file
164
lockbox/cipher/mode/cbc.lua
Normal file
@@ -0,0 +1,164 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CBC = {};
|
||||
|
||||
CBC.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = Array.XOR(iv, block);
|
||||
out = blockCipher.encrypt(key, out);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = out;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
CBC.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = block;
|
||||
out = blockCipher.decrypt(key, out);
|
||||
out = Array.XOR(iv, out);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = block;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return CBC;
|
||||
|
||||
163
lockbox/cipher/mode/cfb.lua
Normal file
163
lockbox/cipher/mode/cfb.lua
Normal file
@@ -0,0 +1,163 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CFB = {};
|
||||
|
||||
CFB.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = out;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
CFB.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = block;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return CFB;
|
||||
248
lockbox/cipher/mode/ctr.lua
Normal file
248
lockbox/cipher/mode/ctr.lua
Normal file
@@ -0,0 +1,248 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local AND = Bit.band;
|
||||
|
||||
local CTR = {};
|
||||
|
||||
CTR.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
local updateIV = function()
|
||||
iv[16] = iv[16] + 1;
|
||||
if iv[16] <= 0xFF then return; end
|
||||
iv[16] = AND(iv[16], 0xFF);
|
||||
|
||||
iv[15] = iv[15] + 1;
|
||||
if iv[15] <= 0xFF then return; end
|
||||
iv[15] = AND(iv[15], 0xFF);
|
||||
|
||||
iv[14] = iv[14] + 1;
|
||||
if iv[14] <= 0xFF then return; end
|
||||
iv[14] = AND(iv[14], 0xFF);
|
||||
|
||||
iv[13] = iv[13] + 1;
|
||||
if iv[13] <= 0xFF then return; end
|
||||
iv[13] = AND(iv[13], 0xFF);
|
||||
|
||||
iv[12] = iv[12] + 1;
|
||||
if iv[12] <= 0xFF then return; end
|
||||
iv[12] = AND(iv[12], 0xFF);
|
||||
|
||||
iv[11] = iv[11] + 1;
|
||||
if iv[11] <= 0xFF then return; end
|
||||
iv[11] = AND(iv[11], 0xFF);
|
||||
|
||||
iv[10] = iv[10] + 1;
|
||||
if iv[10] <= 0xFF then return; end
|
||||
iv[10] = AND(iv[10], 0xFF);
|
||||
|
||||
iv[9] = iv[9] + 1;
|
||||
if iv[9] <= 0xFF then return; end
|
||||
iv[9] = AND(iv[9], 0xFF);
|
||||
|
||||
return;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
updateIV();
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
CTR.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
local updateIV = function()
|
||||
iv[16] = iv[16] + 1;
|
||||
if iv[16] <= 0xFF then return; end
|
||||
iv[16] = AND(iv[16], 0xFF);
|
||||
|
||||
iv[15] = iv[15] + 1;
|
||||
if iv[15] <= 0xFF then return; end
|
||||
iv[15] = AND(iv[15], 0xFF);
|
||||
|
||||
iv[14] = iv[14] + 1;
|
||||
if iv[14] <= 0xFF then return; end
|
||||
iv[14] = AND(iv[14], 0xFF);
|
||||
|
||||
iv[13] = iv[13] + 1;
|
||||
if iv[13] <= 0xFF then return; end
|
||||
iv[13] = AND(iv[13], 0xFF);
|
||||
|
||||
iv[12] = iv[12] + 1;
|
||||
if iv[12] <= 0xFF then return; end
|
||||
iv[12] = AND(iv[12], 0xFF);
|
||||
|
||||
iv[11] = iv[11] + 1;
|
||||
if iv[11] <= 0xFF then return; end
|
||||
iv[11] = AND(iv[11], 0xFF);
|
||||
|
||||
iv[10] = iv[10] + 1;
|
||||
if iv[10] <= 0xFF then return; end
|
||||
iv[10] = AND(iv[10], 0xFF);
|
||||
|
||||
iv[9] = iv[9] + 1;
|
||||
if iv[9] <= 0xFF then return; end
|
||||
iv[9] = AND(iv[9], 0xFF);
|
||||
|
||||
return;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
updateIV();
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return CTR;
|
||||
|
||||
164
lockbox/cipher/mode/ofb.lua
Normal file
164
lockbox/cipher/mode/ofb.lua
Normal file
@@ -0,0 +1,164 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local OFB = {};
|
||||
|
||||
OFB.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
iv = out;
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
OFB.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
iv = out;
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
return OFB;
|
||||
173
lockbox/digest/sha1.lua
Normal file
173
lockbox/digest/sha1.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
require("lockbox").insecure();
|
||||
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local XOR = Bit.bxor;
|
||||
local LROT = Bit.lrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--SHA1 is big-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b0; i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b3);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(i);
|
||||
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
local F = function(x, y, z) return XOR(z, AND(x, XOR(y, z))); end
|
||||
local G = function(x, y, z) return XOR(x, XOR(y, z)); end
|
||||
local H = function(x, y, z) return OR(AND(x, OR(y, z)), AND(y, z)); end
|
||||
local I = function(x, y, z) return XOR(x, XOR(y, z)); end
|
||||
|
||||
local SHA1 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local h0 = 0x67452301;
|
||||
local h1 = 0xEFCDAB89;
|
||||
local h2 = 0x98BADCFE;
|
||||
local h3 = 0x10325476;
|
||||
local h4 = 0xC3D2E1F0;
|
||||
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = h0;
|
||||
local b = h1;
|
||||
local c = h2;
|
||||
local d = h3;
|
||||
local e = h4;
|
||||
local temp;
|
||||
local k;
|
||||
|
||||
local w = {};
|
||||
for i = 0, 15 do
|
||||
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 16, 79 do
|
||||
w[i] = LROT((XOR(XOR(w[i - 3], w[i - 8]), XOR(w[i - 14], w[i - 16]))), 1);
|
||||
end
|
||||
|
||||
for i = 0, 79 do
|
||||
if (i <= 19) then
|
||||
temp = F(b, c, d);
|
||||
k = 0x5A827999;
|
||||
elseif (i <= 39) then
|
||||
temp = G(b, c, d);
|
||||
k = 0x6ED9EBA1;
|
||||
elseif (i <= 59) then
|
||||
temp = H(b, c, d);
|
||||
k = 0x8F1BBCDC;
|
||||
else
|
||||
temp = I(b, c, d);
|
||||
k = 0xCA62C1D6;
|
||||
end
|
||||
temp = LROT(a, 5) + temp + e + k + w[i];
|
||||
e = d;
|
||||
d = c;
|
||||
c = LROT(b, 30);
|
||||
b = a;
|
||||
a = temp;
|
||||
end
|
||||
|
||||
h0 = AND(h0 + a, 0xFFFFFFFF);
|
||||
h1 = AND(h1 + b, 0xFFFFFFFF);
|
||||
h2 = AND(h2 + c, 0xFFFFFFFF);
|
||||
h3 = AND(h3 + d, 0xFFFFFFFF);
|
||||
h4 = AND(h4 + e, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
h0 = 0x67452301;
|
||||
h1 = 0xEFCDAB89;
|
||||
h2 = 0x98BADCFE;
|
||||
h3 = 0x10325476;
|
||||
h4 = 0xC3D2E1F0;
|
||||
return public;
|
||||
end
|
||||
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if queue.size() >= 64 then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
|
||||
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
|
||||
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19);
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
return SHA1;
|
||||
200
lockbox/digest/sha2_224.lua
Normal file
200
lockbox/digest/sha2_224.lua
Normal file
@@ -0,0 +1,200 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CONSTANTS = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
|
||||
|
||||
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local NOT = Bit.bnot;
|
||||
local XOR = Bit.bxor;
|
||||
local RROT = Bit.rrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--SHA2 is big-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b0; i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b3);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(i);
|
||||
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
local SHA2_224 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local h0 = 0xc1059ed8;
|
||||
local h1 = 0x367cd507;
|
||||
local h2 = 0x3070dd17;
|
||||
local h3 = 0xf70e5939;
|
||||
local h4 = 0xffc00b31;
|
||||
local h5 = 0x68581511;
|
||||
local h6 = 0x64f98fa7;
|
||||
local h7 = 0xbefa4fa4;
|
||||
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = h0;
|
||||
local b = h1;
|
||||
local c = h2;
|
||||
local d = h3;
|
||||
local e = h4;
|
||||
local f = h5;
|
||||
local g = h6;
|
||||
local h = h7;
|
||||
|
||||
local w = {};
|
||||
|
||||
for i = 0, 15 do
|
||||
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 16, 63 do
|
||||
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
|
||||
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
|
||||
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
for i = 0, 63 do
|
||||
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
|
||||
local ch = XOR(AND(e, f), AND(NOT(e), g));
|
||||
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
|
||||
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
|
||||
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
|
||||
local temp2 = s0 + maj;
|
||||
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + temp1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = temp1 + temp2;
|
||||
end
|
||||
|
||||
h0 = AND(h0 + a, 0xFFFFFFFF);
|
||||
h1 = AND(h1 + b, 0xFFFFFFFF);
|
||||
h2 = AND(h2 + c, 0xFFFFFFFF);
|
||||
h3 = AND(h3 + d, 0xFFFFFFFF);
|
||||
h4 = AND(h4 + e, 0xFFFFFFFF);
|
||||
h5 = AND(h5 + f, 0xFFFFFFFF);
|
||||
h6 = AND(h6 + g, 0xFFFFFFFF);
|
||||
h7 = AND(h7 + h, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
|
||||
h0 = 0xc1059ed8;
|
||||
h1 = 0x367cd507;
|
||||
h2 = 0x3070dd17;
|
||||
h3 = 0xf70e5939;
|
||||
h4 = 0xffc00b31;
|
||||
h5 = 0x68581511;
|
||||
h6 = 0x64f98fa7;
|
||||
h7 = 0xbefa4fa4;
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if queue.size() >= 64 then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
|
||||
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
|
||||
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return SHA2_224;
|
||||
|
||||
203
lockbox/digest/sha2_256.lua
Normal file
203
lockbox/digest/sha2_256.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CONSTANTS = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
|
||||
|
||||
local fmt = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ..
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local NOT = Bit.bnot;
|
||||
local XOR = Bit.bxor;
|
||||
local RROT = Bit.rrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--SHA2 is big-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b0; i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b3);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(i);
|
||||
local b0, b1, b2, b3 = word2bytes(Math.floor(i / 0x100000000));
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
local SHA2_256 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local h0 = 0x6a09e667;
|
||||
local h1 = 0xbb67ae85;
|
||||
local h2 = 0x3c6ef372;
|
||||
local h3 = 0xa54ff53a;
|
||||
local h4 = 0x510e527f;
|
||||
local h5 = 0x9b05688c;
|
||||
local h6 = 0x1f83d9ab;
|
||||
local h7 = 0x5be0cd19;
|
||||
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = h0;
|
||||
local b = h1;
|
||||
local c = h2;
|
||||
local d = h3;
|
||||
local e = h4;
|
||||
local f = h5;
|
||||
local g = h6;
|
||||
local h = h7;
|
||||
|
||||
local w = {};
|
||||
|
||||
for i = 0, 15 do
|
||||
w[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 16, 63 do
|
||||
local s0 = XOR(RROT(w[i - 15], 7), XOR(RROT(w[i - 15], 18), RSHIFT(w[i - 15], 3)));
|
||||
local s1 = XOR(RROT(w[i - 2], 17), XOR(RROT(w[i - 2], 19), RSHIFT(w[i - 2], 10)));
|
||||
w[i] = AND(w[i - 16] + s0 + w[i - 7] + s1, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
for i = 0, 63 do
|
||||
local s1 = XOR(RROT(e, 6), XOR(RROT(e, 11), RROT(e, 25)));
|
||||
local ch = XOR(AND(e, f), AND(NOT(e), g));
|
||||
local temp1 = h + s1 + ch + CONSTANTS[i + 1] + w[i];
|
||||
local s0 = XOR(RROT(a, 2), XOR(RROT(a, 13), RROT(a, 22)));
|
||||
local maj = XOR(AND(a, b), XOR(AND(a, c), AND(b, c)));
|
||||
local temp2 = s0 + maj;
|
||||
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + temp1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = temp1 + temp2;
|
||||
end
|
||||
|
||||
h0 = AND(h0 + a, 0xFFFFFFFF);
|
||||
h1 = AND(h1 + b, 0xFFFFFFFF);
|
||||
h2 = AND(h2 + c, 0xFFFFFFFF);
|
||||
h3 = AND(h3 + d, 0xFFFFFFFF);
|
||||
h4 = AND(h4 + e, 0xFFFFFFFF);
|
||||
h5 = AND(h5 + f, 0xFFFFFFFF);
|
||||
h6 = AND(h6 + g, 0xFFFFFFFF);
|
||||
h7 = AND(h7 + h, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
|
||||
h0 = 0x6a09e667;
|
||||
h1 = 0xbb67ae85;
|
||||
h2 = 0x3c6ef372;
|
||||
h3 = 0xa54ff53a;
|
||||
h4 = 0x510e527f;
|
||||
h5 = 0x9b05688c;
|
||||
h6 = 0x1f83d9ab;
|
||||
h7 = 0x5be0cd19;
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if queue.size() >= 64 then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
local b28, b29, b30, b31 = word2bytes(h7);
|
||||
|
||||
|
||||
return { b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(h0);
|
||||
local b4, b5, b6, b7 = word2bytes(h1);
|
||||
local b8, b9, b10, b11 = word2bytes(h2);
|
||||
local b12, b13, b14, b15 = word2bytes(h3);
|
||||
local b16, b17, b18, b19 = word2bytes(h4);
|
||||
local b20, b21, b22, b23 = word2bytes(h5);
|
||||
local b24, b25, b26, b27 = word2bytes(h6);
|
||||
local b28, b29, b30, b31 = word2bytes(h7);
|
||||
|
||||
return String.format(fmt, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15
|
||||
, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return SHA2_256;
|
||||
|
||||
22
lockbox/init.lua
Normal file
22
lockbox/init.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
local Lockbox = {};
|
||||
|
||||
--[[
|
||||
package.path = "./?.lua;"
|
||||
.. "./cipher/?.lua;"
|
||||
.. "./digest/?.lua;"
|
||||
.. "./kdf/?.lua;"
|
||||
.. "./mac/?.lua;"
|
||||
.. "./padding/?.lua;"
|
||||
.. "./test/?.lua;"
|
||||
.. "./util/?.lua;"
|
||||
.. package.path;
|
||||
--]]
|
||||
Lockbox.ALLOW_INSECURE = true;
|
||||
|
||||
Lockbox.insecure = function()
|
||||
assert(Lockbox.ALLOW_INSECURE,
|
||||
"This module is insecure! It should not be used in production." ..
|
||||
"If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it");
|
||||
end
|
||||
|
||||
return Lockbox;
|
||||
114
lockbox/kdf/pbkdf2.lua
Normal file
114
lockbox/kdf/pbkdf2.lua
Normal file
@@ -0,0 +1,114 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Math = require("math");
|
||||
|
||||
local AND = Bit.band;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b3 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b0 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local PBKDF2 = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local blockLen = 16;
|
||||
local dKeyLen = 256;
|
||||
local iterations = 4096;
|
||||
|
||||
local salt;
|
||||
local password;
|
||||
|
||||
|
||||
local PRF;
|
||||
|
||||
local dKey;
|
||||
|
||||
|
||||
public.setBlockLen = function(len)
|
||||
blockLen = len;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setDKeyLen = function(len)
|
||||
dKeyLen = len
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setIterations = function(iter)
|
||||
iterations = iter;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setSalt = function(saltBytes)
|
||||
salt = saltBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPassword = function(passwordBytes)
|
||||
password = passwordBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPRF = function(prf)
|
||||
PRF = prf;
|
||||
return public;
|
||||
end
|
||||
|
||||
local buildBlock = function(i)
|
||||
local b0, b1, b2, b3 = word2bytes(i);
|
||||
local ii = {b0, b1, b2, b3};
|
||||
local s = Array.concat(salt, ii);
|
||||
|
||||
local out = {};
|
||||
|
||||
PRF.setKey(password);
|
||||
for c = 1, iterations do
|
||||
PRF.init()
|
||||
.update(Stream.fromArray(s));
|
||||
|
||||
s = PRF.finish().asBytes();
|
||||
if(c > 1) then
|
||||
out = Array.XOR(out, s);
|
||||
else
|
||||
out = s;
|
||||
end
|
||||
end
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local blocks = Math.ceil(dKeyLen / blockLen);
|
||||
|
||||
dKey = {};
|
||||
|
||||
for b = 1, blocks do
|
||||
local block = buildBlock(b);
|
||||
dKey = Array.concat(dKey, block);
|
||||
end
|
||||
|
||||
if(Array.size(dKey) > dKeyLen) then dKey = Array.truncate(dKey, dKeyLen); end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return dKey;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Array.toHex(dKey);
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
return PBKDF2;
|
||||
85
lockbox/mac/hmac.lua
Normal file
85
lockbox/mac/hmac.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Array = require("lockbox.util.array");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local HMAC = function()
|
||||
|
||||
local public = {};
|
||||
local blockSize = 64;
|
||||
local Digest = nil;
|
||||
local outerPadding = {};
|
||||
local innerPadding = {}
|
||||
local digest;
|
||||
|
||||
public.setBlockSize = function(bytes)
|
||||
blockSize = bytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setDigest = function(digestModule)
|
||||
Digest = digestModule;
|
||||
digest = Digest();
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setKey = function(key)
|
||||
local keyStream;
|
||||
|
||||
if(Array.size(key) > blockSize) then
|
||||
keyStream = Stream.fromArray(Digest()
|
||||
.update(Stream.fromArray(key))
|
||||
.finish()
|
||||
.asBytes());
|
||||
else
|
||||
keyStream = Stream.fromArray(key);
|
||||
end
|
||||
|
||||
outerPadding = {};
|
||||
innerPadding = {};
|
||||
|
||||
for i = 1, blockSize do
|
||||
local byte = keyStream();
|
||||
if byte == nil then byte = 0x00; end
|
||||
outerPadding[i] = XOR(0x5C, byte);
|
||||
innerPadding[i] = XOR(0x36, byte);
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
digest.init()
|
||||
.update(Stream.fromArray(innerPadding));
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
digest.update(messageStream);
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local inner = digest.finish().asBytes();
|
||||
digest.init()
|
||||
.update(Stream.fromArray(outerPadding))
|
||||
.update(Stream.fromArray(inner))
|
||||
.finish();
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return digest.asBytes();
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return digest.asHex();
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return HMAC;
|
||||
22
lockbox/padding/ansix923.lua
Normal file
22
lockbox/padding/ansix923.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
local ANSIX923Padding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - (byteCount % blockSize);
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft > 1 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x00;
|
||||
elseif bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return paddingCount;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
|
||||
end
|
||||
|
||||
return ANSIX923Padding;
|
||||
22
lockbox/padding/isoiec7816.lua
Normal file
22
lockbox/padding/isoiec7816.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
local ISOIEC7816Padding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - (byteCount % blockSize);
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft == paddingCount then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x80;
|
||||
elseif bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x00;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
|
||||
end
|
||||
|
||||
return ISOIEC7816Padding;
|
||||
18
lockbox/padding/pkcs7.lua
Normal file
18
lockbox/padding/pkcs7.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local PKCS7Padding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return paddingCount;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
end
|
||||
|
||||
return PKCS7Padding;
|
||||
19
lockbox/padding/zero.lua
Normal file
19
lockbox/padding/zero.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
local ZeroPadding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x00;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
|
||||
end
|
||||
|
||||
return ZeroPadding;
|
||||
211
lockbox/util/array.lua
Normal file
211
lockbox/util/array.lua
Normal file
@@ -0,0 +1,211 @@
|
||||
|
||||
local String = require("string");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local Array = {};
|
||||
|
||||
Array.size = function(array)
|
||||
return #array;
|
||||
end
|
||||
|
||||
Array.fromString = function(string)
|
||||
local bytes = {};
|
||||
|
||||
local i = 1;
|
||||
local byte = String.byte(string, i);
|
||||
while byte ~= nil do
|
||||
bytes[i] = byte;
|
||||
i = i + 1;
|
||||
byte = String.byte(string, i);
|
||||
end
|
||||
|
||||
return bytes;
|
||||
|
||||
end
|
||||
|
||||
Array.toString = function(bytes)
|
||||
local chars = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = bytes[i];
|
||||
while byte ~= nil do
|
||||
chars[i] = String.char(byte);
|
||||
i = i + 1;
|
||||
byte = bytes[i];
|
||||
end
|
||||
|
||||
return table.concat(chars, "");
|
||||
end
|
||||
|
||||
Array.fromStream = function(stream)
|
||||
local array = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
array[i] = byte;
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
Array.readFromQueue = function(queue, size)
|
||||
local array = {};
|
||||
|
||||
for i = 1, size do
|
||||
array[i] = queue.pop();
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
Array.writeToQueue = function(queue, array)
|
||||
local size = Array.size(array);
|
||||
|
||||
for i = 1, size do
|
||||
queue.push(array[i]);
|
||||
end
|
||||
end
|
||||
|
||||
Array.toStream = function(array)
|
||||
local queue = Queue();
|
||||
local i = 1;
|
||||
|
||||
local byte = array[i];
|
||||
while byte ~= nil do
|
||||
queue.push(byte);
|
||||
i = i + 1;
|
||||
byte = array[i];
|
||||
end
|
||||
|
||||
return queue.pop;
|
||||
end
|
||||
|
||||
|
||||
local fromHexTable = {};
|
||||
for i = 0, 255 do
|
||||
fromHexTable[String.format("%02X", i)] = i;
|
||||
fromHexTable[String.format("%02x", i)] = i;
|
||||
end
|
||||
|
||||
Array.fromHex = function(hex)
|
||||
local array = {};
|
||||
|
||||
for i = 1, String.len(hex) / 2 do
|
||||
local h = String.sub(hex, i * 2 - 1, i * 2);
|
||||
array[i] = fromHexTable[h];
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
|
||||
local toHexTable = {};
|
||||
for i = 0, 255 do
|
||||
toHexTable[i] = String.format("%02X", i);
|
||||
end
|
||||
|
||||
Array.toHex = function(array)
|
||||
local hex = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = array[i];
|
||||
while byte ~= nil do
|
||||
hex[i] = toHexTable[byte];
|
||||
i = i + 1;
|
||||
byte = array[i];
|
||||
end
|
||||
|
||||
return table.concat(hex, "");
|
||||
|
||||
end
|
||||
|
||||
Array.concat = function(a, b)
|
||||
local concat = {};
|
||||
local out = 1;
|
||||
|
||||
local i = 1;
|
||||
local byte = a[i];
|
||||
while byte ~= nil do
|
||||
concat[out] = byte;
|
||||
i = i + 1;
|
||||
out = out + 1;
|
||||
byte = a[i];
|
||||
end
|
||||
|
||||
i = 1;
|
||||
byte = b[i];
|
||||
while byte ~= nil do
|
||||
concat[out] = byte;
|
||||
i = i + 1;
|
||||
out = out + 1;
|
||||
byte = b[i];
|
||||
end
|
||||
|
||||
return concat;
|
||||
end
|
||||
|
||||
Array.truncate = function(a, newSize)
|
||||
local x = {};
|
||||
|
||||
for i = 1, newSize do
|
||||
x[i] = a[i];
|
||||
end
|
||||
|
||||
return x;
|
||||
end
|
||||
|
||||
Array.XOR = function(a, b)
|
||||
local x = {};
|
||||
|
||||
for k, v in pairs(a) do
|
||||
x[k] = XOR(v, b[k]);
|
||||
end
|
||||
|
||||
return x;
|
||||
end
|
||||
|
||||
Array.substitute = function(input, sbox)
|
||||
local out = {};
|
||||
|
||||
for k, v in pairs(input) do
|
||||
out[k] = sbox[v];
|
||||
end
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
Array.permute = function(input, pbox)
|
||||
local out = {};
|
||||
|
||||
for k, v in pairs(pbox) do
|
||||
out[k] = input[v];
|
||||
end
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
Array.copy = function(input)
|
||||
local out = {};
|
||||
|
||||
for k, v in pairs(input) do
|
||||
out[k] = v;
|
||||
end
|
||||
return out;
|
||||
end
|
||||
|
||||
Array.slice = function(input, start, stop)
|
||||
local out = {};
|
||||
|
||||
for i = start, stop do
|
||||
out[i - start + 1] = input[i];
|
||||
end
|
||||
return out;
|
||||
end
|
||||
|
||||
return Array;
|
||||
25
lockbox/util/bit.lua
Normal file
25
lockbox/util/bit.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
local ok, e
|
||||
ok = nil
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit") -- the LuaJIT one ?
|
||||
end
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit32") -- Lua 5.2
|
||||
end
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit.numberlua") -- for Lua 5.1, https://github.com/tst2005/lua-bit-numberlua/
|
||||
end
|
||||
if not ok then
|
||||
error("no bitwise support found", 2)
|
||||
end
|
||||
assert(type(e) == "table", "invalid bit module")
|
||||
|
||||
-- Workaround to support Lua 5.2 bit32 API with the LuaJIT bit one
|
||||
if e.rol and not e.lrotate then
|
||||
e.lrotate = e.rol
|
||||
end
|
||||
if e.ror and not e.rrotate then
|
||||
e.rrotate = e.ror
|
||||
end
|
||||
|
||||
return e
|
||||
47
lockbox/util/queue.lua
Normal file
47
lockbox/util/queue.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
local Queue = function()
|
||||
local queue = {};
|
||||
local tail = 0;
|
||||
local head = 0;
|
||||
|
||||
local public = {};
|
||||
|
||||
public.push = function(obj)
|
||||
queue[head] = obj;
|
||||
head = head + 1;
|
||||
return;
|
||||
end
|
||||
|
||||
public.pop = function()
|
||||
if tail < head
|
||||
then
|
||||
local obj = queue[tail];
|
||||
queue[tail] = nil;
|
||||
tail = tail + 1;
|
||||
return obj;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
public.size = function()
|
||||
return head - tail;
|
||||
end
|
||||
|
||||
public.getHead = function()
|
||||
return head;
|
||||
end
|
||||
|
||||
public.getTail = function()
|
||||
return tail;
|
||||
end
|
||||
|
||||
public.reset = function()
|
||||
queue = {};
|
||||
head = 0;
|
||||
tail = 0;
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
return Queue;
|
||||
99
lockbox/util/stream.lua
Normal file
99
lockbox/util/stream.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
local Queue = require("lockbox.util.queue");
|
||||
local String = require("string");
|
||||
|
||||
local Stream = {};
|
||||
|
||||
|
||||
Stream.fromString = function(string)
|
||||
local i = 0;
|
||||
return function()
|
||||
i = i + 1;
|
||||
return String.byte(string, i);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Stream.toString = function(stream)
|
||||
local array = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
array[i] = String.char(byte);
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return table.concat(array);
|
||||
end
|
||||
|
||||
|
||||
Stream.fromArray = function(array)
|
||||
local queue = Queue();
|
||||
local i = 1;
|
||||
|
||||
local byte = array[i];
|
||||
while byte ~= nil do
|
||||
queue.push(byte);
|
||||
i = i + 1;
|
||||
byte = array[i];
|
||||
end
|
||||
|
||||
return queue.pop;
|
||||
end
|
||||
|
||||
|
||||
Stream.toArray = function(stream)
|
||||
local array = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
array[i] = byte;
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return array;
|
||||
end
|
||||
|
||||
|
||||
local fromHexTable = {};
|
||||
for i = 0, 255 do
|
||||
fromHexTable[String.format("%02X", i)] = i;
|
||||
fromHexTable[String.format("%02x", i)] = i;
|
||||
end
|
||||
|
||||
Stream.fromHex = function(hex)
|
||||
local queue = Queue();
|
||||
|
||||
for i = 1, String.len(hex) / 2 do
|
||||
local h = String.sub(hex, i * 2 - 1, i * 2);
|
||||
queue.push(fromHexTable[h]);
|
||||
end
|
||||
|
||||
return queue.pop;
|
||||
end
|
||||
|
||||
|
||||
|
||||
local toHexTable = {};
|
||||
for i = 0, 255 do
|
||||
toHexTable[i] = String.format("%02X", i);
|
||||
end
|
||||
|
||||
Stream.toHex = function(stream)
|
||||
local hex = {};
|
||||
local i = 1;
|
||||
|
||||
local byte = stream();
|
||||
while byte ~= nil do
|
||||
hex[i] = toHexTable[byte];
|
||||
i = i + 1;
|
||||
byte = stream();
|
||||
end
|
||||
|
||||
return table.concat(hex);
|
||||
end
|
||||
|
||||
return Stream;
|
||||
@@ -1,135 +0,0 @@
|
||||
-- mekanism reactor controller
|
||||
-- monitors and regulates mekanism reactors
|
||||
|
||||
os.loadAPI("reactor.lua")
|
||||
os.loadAPI("defs.lua")
|
||||
os.loadAPI("log.lua")
|
||||
os.loadAPI("render.lua")
|
||||
os.loadAPI("server.lua")
|
||||
os.loadAPI("regulator.lua")
|
||||
|
||||
-- constants, aliases, properties
|
||||
local header = "MEKANISM REACTOR CONTROLLER - v" .. defs.CTRL_VERSION
|
||||
local monitor_0 = peripheral.wrap(defs.MONITOR_0)
|
||||
local monitor_1 = peripheral.wrap(defs.MONITOR_1)
|
||||
local monitor_2 = peripheral.wrap(defs.MONITOR_2)
|
||||
local monitor_3 = peripheral.wrap(defs.MONITOR_3)
|
||||
|
||||
monitor_0.setBackgroundColor(colors.black)
|
||||
monitor_0.setTextColor(colors.white)
|
||||
monitor_0.clear()
|
||||
|
||||
monitor_1.setBackgroundColor(colors.black)
|
||||
monitor_1.setTextColor(colors.white)
|
||||
monitor_1.clear()
|
||||
|
||||
monitor_2.setBackgroundColor(colors.black)
|
||||
monitor_2.setTextColor(colors.white)
|
||||
monitor_2.clear()
|
||||
|
||||
log.init(monitor_3)
|
||||
|
||||
local main_w, main_h = monitor_0.getSize()
|
||||
local view = window.create(monitor_0, 1, 1, main_w, main_h)
|
||||
view.setBackgroundColor(colors.black)
|
||||
view.clear()
|
||||
|
||||
local stat_w, stat_h = monitor_1.getSize()
|
||||
local stat_view = window.create(monitor_1, 1, 1, stat_w, stat_h)
|
||||
stat_view.setBackgroundColor(colors.black)
|
||||
stat_view.clear()
|
||||
|
||||
local reactors = {
|
||||
reactor.create(1, view, stat_view, 62, 3, 63, 2),
|
||||
reactor.create(2, view, stat_view, 42, 3, 43, 2),
|
||||
reactor.create(3, view, stat_view, 22, 3, 23, 2),
|
||||
reactor.create(4, view, stat_view, 2, 3, 3, 2)
|
||||
}
|
||||
print("[debug] reactor tables created")
|
||||
|
||||
server.init(reactors)
|
||||
print("[debug] modem server started")
|
||||
|
||||
regulator.init(reactors)
|
||||
print("[debug] regulator started")
|
||||
|
||||
-- header
|
||||
view.setBackgroundColor(colors.white)
|
||||
view.setTextColor(colors.black)
|
||||
view.setCursorPos(1, 1)
|
||||
local header_pad_x = (main_w - string.len(header)) / 2
|
||||
view.write(string.rep(" ", header_pad_x) .. header .. string.rep(" ", header_pad_x))
|
||||
|
||||
-- inital draw of each reactor
|
||||
for key, rctr in pairs(reactors) do
|
||||
render.draw_reactor_system(rctr)
|
||||
render.draw_reactor_status(rctr)
|
||||
end
|
||||
|
||||
-- inital draw of clock
|
||||
monitor_2.setTextScale(2)
|
||||
monitor_2.setCursorPos(1, 1)
|
||||
monitor_2.write(os.date("%Y/%m/%d %H:%M:%S"))
|
||||
|
||||
local clock_update_timer = os.startTimer(1)
|
||||
|
||||
while true do
|
||||
event, param1, param2, param3, param4, param5 = os.pullEvent()
|
||||
|
||||
if event == "redstone" then
|
||||
-- redstone state change
|
||||
regulator.handle_redstone()
|
||||
elseif event == "modem_message" then
|
||||
-- received signal router packet
|
||||
packet = {
|
||||
side = param1,
|
||||
sender = param2,
|
||||
reply = param3,
|
||||
message = param4,
|
||||
distance = param5
|
||||
}
|
||||
|
||||
server.handle_message(packet, reactors)
|
||||
elseif event == "monitor_touch" then
|
||||
if param1 == "monitor_5" then
|
||||
local tap_x = param2
|
||||
local tap_y = param3
|
||||
|
||||
for key, rctr in pairs(reactors) do
|
||||
if tap_x >= rctr.render.stat_x and tap_x <= (rctr.render.stat_x + 15) then
|
||||
local old_val = rctr.waste_production
|
||||
-- width in range
|
||||
if tap_y == (rctr.render.stat_y + 12) then
|
||||
rctr.waste_production = "plutonium"
|
||||
elseif tap_y == (rctr.render.stat_y + 14) then
|
||||
rctr.waste_production = "polonium"
|
||||
elseif tap_y == (rctr.render.stat_y + 16) then
|
||||
rctr.waste_production = "antimatter"
|
||||
end
|
||||
|
||||
-- notify reactor of changes
|
||||
if old_val ~= rctr.waste_production then
|
||||
server.send(rctr.id, rctr.waste_production)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
-- update the clock about every second
|
||||
monitor_2.setCursorPos(1, 1)
|
||||
monitor_2.write(os.date("%Y/%m/%d %H:%M:%S"))
|
||||
clock_update_timer = os.startTimer(1)
|
||||
|
||||
-- send keep-alive
|
||||
server.broadcast(1)
|
||||
end
|
||||
|
||||
-- update reactor display
|
||||
for key, rctr in pairs(reactors) do
|
||||
render.draw_reactor_system(rctr)
|
||||
render.draw_reactor_status(rctr)
|
||||
end
|
||||
|
||||
-- update system status monitor
|
||||
render.update_system_monitor(monitor_2, regulator.is_scrammed(), reactors)
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
-- configuration definitions
|
||||
|
||||
CTRL_VERSION = "0.7"
|
||||
|
||||
-- monitors
|
||||
MONITOR_0 = "monitor_6"
|
||||
MONITOR_1 = "monitor_5"
|
||||
MONITOR_2 = "monitor_7"
|
||||
MONITOR_3 = "monitor_8"
|
||||
|
||||
-- modem server
|
||||
LISTEN_PORT = 1000
|
||||
|
||||
-- regulator (should match the number of reactors present)
|
||||
BUNDLE_DEF = { colors.red, colors.orange, colors.yellow, colors.lime }
|
||||
|
||||
-- stats calculation
|
||||
REACTOR_MB_T = 39
|
||||
TURBINE_MRF_T = 3.114
|
||||
PLUTONIUM_PER_WASTE = 0.1
|
||||
POLONIUM_PER_WASTE = 0.1
|
||||
SPENT_PER_BYPRODUCT = 1
|
||||
ANTIMATTER_PER_POLONIUM = 0.001
|
||||
52
main/log.lua
52
main/log.lua
@@ -1,52 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
|
||||
local out, out_w, out_h
|
||||
local output_full = false
|
||||
|
||||
-- initialize the logger to the given monitor
|
||||
-- monitor: monitor to write to (in addition to calling print())
|
||||
function init(monitor)
|
||||
out = monitor
|
||||
out_w, out_h = out.getSize()
|
||||
|
||||
out.clear()
|
||||
out.setTextColor(colors.white)
|
||||
out.setBackgroundColor(colors.black)
|
||||
|
||||
out.setCursorPos(1, 1)
|
||||
out.write("version " .. defs.CTRL_VERSION)
|
||||
out.setCursorPos(1, 2)
|
||||
out.write("system startup at " .. os.date("%Y/%m/%d %H:%M:%S"))
|
||||
|
||||
print("server v" .. defs.CTRL_VERSION .. " started at " .. os.date("%Y/%m/%d %H:%M:%S"))
|
||||
end
|
||||
|
||||
-- write a log message to the log screen and console
|
||||
-- msg: message to write
|
||||
-- color: (optional) color to print in, defaults to white
|
||||
function write(msg, color)
|
||||
color = color or colors.white
|
||||
local _x, _y = out.getCursorPos()
|
||||
|
||||
if output_full then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, _y)
|
||||
else
|
||||
if _y == out_h then
|
||||
output_full = true
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, _y)
|
||||
else
|
||||
out.setCursorPos(1, _y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- output to screen
|
||||
out.setTextColor(colors.lightGray)
|
||||
out.write(os.date("[%H:%M:%S] "))
|
||||
out.setTextColor(color)
|
||||
out.write(msg)
|
||||
|
||||
-- output to console
|
||||
print(os.date("[%H:%M:%S] ") .. msg)
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
-- create a new reactor 'object'
|
||||
-- reactor_id: the ID for this reactor
|
||||
-- main_view: the parent window/monitor for the main display (components)
|
||||
-- status_view: the parent window/monitor for the status display
|
||||
-- main_x: where to create the main window, x coordinate
|
||||
-- main_y: where to create the main window, y coordinate
|
||||
-- status_x: where to create the status window, x coordinate
|
||||
-- status_y: where to create the status window, y coordinate
|
||||
function create(reactor_id, main_view, status_view, main_x, main_y, status_x, status_y)
|
||||
return {
|
||||
id = reactor_id,
|
||||
render = {
|
||||
win_main = window.create(main_view, main_x, main_y, 20, 60, true),
|
||||
win_stat = window.create(status_view, status_x, status_y, 20, 20, true),
|
||||
stat_x = status_x,
|
||||
stat_y = status_y
|
||||
},
|
||||
control_state = false,
|
||||
waste_production = "antimatter", -- "plutonium", "polonium", "antimatter"
|
||||
state = {
|
||||
run = false,
|
||||
no_fuel = false,
|
||||
full_waste = false,
|
||||
high_temp = false,
|
||||
damage_crit = false
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
os.loadAPI("log.lua")
|
||||
os.loadAPI("server.lua")
|
||||
|
||||
local reactors
|
||||
local scrammed
|
||||
local auto_scram
|
||||
|
||||
-- initialize the system regulator which provides safety measures, SCRAM functionality, and handles redstone
|
||||
-- _reactors: reactor table
|
||||
function init(_reactors)
|
||||
reactors = _reactors
|
||||
scrammed = false
|
||||
auto_scram = false
|
||||
|
||||
-- scram all reactors
|
||||
server.broadcast(false, reactors)
|
||||
|
||||
-- check initial states
|
||||
regulator.handle_redstone()
|
||||
end
|
||||
|
||||
-- check if the system is scrammed
|
||||
function is_scrammed()
|
||||
return scrammed
|
||||
end
|
||||
|
||||
-- handle redstone state changes
|
||||
function handle_redstone()
|
||||
-- check scram button
|
||||
if not rs.getInput("right") then
|
||||
if not scrammed then
|
||||
log.write("user SCRAM", colors.red)
|
||||
scram()
|
||||
end
|
||||
|
||||
-- toggling scram will release auto scram state
|
||||
auto_scram = false
|
||||
else
|
||||
scrammed = false
|
||||
end
|
||||
|
||||
-- check individual control buttons
|
||||
local input = rs.getBundledInput("left")
|
||||
for key, rctr in pairs(reactors) do
|
||||
if colors.test(input, defs.BUNDLE_DEF[key]) ~= rctr.control_state then
|
||||
-- state changed
|
||||
rctr.control_state = colors.test(input, defs.BUNDLE_DEF[key])
|
||||
if not scrammed then
|
||||
local safe = true
|
||||
|
||||
if rctr.control_state then
|
||||
safe = check_enable_safety(reactors[key])
|
||||
if safe then
|
||||
log.write("reactor " .. reactors[key].id .. " enabled", colors.lime)
|
||||
end
|
||||
else
|
||||
log.write("reactor " .. reactors[key].id .. " disabled", colors.cyan)
|
||||
end
|
||||
|
||||
-- start/stop reactor
|
||||
if safe then
|
||||
server.send(rctr.id, rctr.control_state)
|
||||
end
|
||||
elseif colors.test(input, defs.BUNDLE_DEF[key]) then
|
||||
log.write("scrammed: state locked off", colors.yellow)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- make sure enabling the provided reactor is safe
|
||||
-- reactor: reactor to check
|
||||
function check_enable_safety(reactor)
|
||||
if reactor.state.no_fuel or reactor.state.full_waste or reactor.state.high_temp or reactor.state.damage_crit then
|
||||
log.write("RCT-" .. reactor.id .. ": unsafe enable denied", colors.yellow)
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- make sure no running reactors are in a bad state
|
||||
function enforce_safeties()
|
||||
for key, reactor in pairs(reactors) do
|
||||
local overridden = false
|
||||
local state = reactor.state
|
||||
|
||||
-- check for problems
|
||||
if state.damage_crit and state.run then
|
||||
reactor.control_state = false
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (damage)", colors.yellow)
|
||||
|
||||
-- scram all, so ignore setting overridden
|
||||
log.write("auto SCRAM all reactors", colors.red)
|
||||
auto_scram = true
|
||||
scram()
|
||||
elseif state.high_temp and state.run then
|
||||
reactor.control_state = false
|
||||
overridden = true
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (temp)", colors.yellow)
|
||||
elseif state.full_waste and state.run then
|
||||
reactor.control_state = false
|
||||
overridden = true
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (waste)", colors.yellow)
|
||||
elseif state.no_fuel and state.run then
|
||||
reactor.control_state = false
|
||||
overridden = true
|
||||
log.write("RCT-" .. reactor.id .. ": shut down (fuel)", colors.yellow)
|
||||
end
|
||||
|
||||
if overridden then
|
||||
server.send(reactor.id, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- shut down all reactors and prevent enabling them until the scram button is toggled/released
|
||||
function scram()
|
||||
scrammed = true
|
||||
server.broadcast(false, reactors)
|
||||
|
||||
for key, rctr in pairs(reactors) do
|
||||
if rctr.control_state then
|
||||
log.write("reactor " .. reactors[key].id .. " disabled", colors.cyan)
|
||||
end
|
||||
end
|
||||
end
|
||||
370
main/render.lua
370
main/render.lua
@@ -1,370 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
|
||||
-- draw pipes between machines
|
||||
-- win: window to render in
|
||||
-- x: starting x coord
|
||||
-- y: starting y coord
|
||||
-- spacing: spacing between the pipes
|
||||
-- color_out: output pipe contents color
|
||||
-- color_ret: return pipe contents color
|
||||
-- tick: tick the pipes for an animation
|
||||
function draw_pipe(win, x, y, spacing, color_out, color_ret, tick)
|
||||
local _color
|
||||
local _off
|
||||
tick = tick or 0
|
||||
|
||||
for i = 0, 4, 1
|
||||
do
|
||||
_off = (i + tick) % 2 == 0 or (tick == 1 and i == 0) or (tick == 3 and i == 4)
|
||||
|
||||
if _off then
|
||||
_color = colors.lightGray
|
||||
else
|
||||
_color = color_out
|
||||
end
|
||||
|
||||
win.setBackgroundColor(_color)
|
||||
win.setCursorPos(x, y + i)
|
||||
win.write(" ")
|
||||
|
||||
if not _off then
|
||||
_color = color_ret
|
||||
end
|
||||
|
||||
win.setBackgroundColor(_color)
|
||||
win.setCursorPos(x + spacing, y + i)
|
||||
win.write(" ")
|
||||
end
|
||||
end
|
||||
|
||||
-- draw a reactor view consisting of the reactor, boiler, turbine, and pipes
|
||||
-- data: reactor table
|
||||
function draw_reactor_system(data)
|
||||
local win = data.render.win_main
|
||||
local win_w, win_h = win.getSize()
|
||||
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.setTextColor(colors.black)
|
||||
win.clear()
|
||||
win.setCursorPos(1, 1)
|
||||
|
||||
-- draw header --
|
||||
|
||||
local header = "REACTOR " .. data.id
|
||||
local header_pad_x = (win_w - string.len(header) - 2) / 2
|
||||
local header_color
|
||||
if data.state.no_fuel then
|
||||
if data.state.run then
|
||||
header_color = colors.purple
|
||||
else
|
||||
header_color = colors.brown
|
||||
end
|
||||
elseif data.state.full_waste then
|
||||
header_color = colors.yellow
|
||||
elseif data.state.high_temp then
|
||||
header_color = colors.orange
|
||||
elseif data.state.damage_crit then
|
||||
header_color = colors.red
|
||||
elseif data.state.run then
|
||||
header_color = colors.green
|
||||
else
|
||||
header_color = colors.lightGray
|
||||
end
|
||||
|
||||
local running = data.state.run and not data.state.no_fuel
|
||||
|
||||
win.write(" ")
|
||||
win.setBackgroundColor(header_color)
|
||||
win.write(string.rep(" ", win_w - 2))
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.write(" ")
|
||||
win.setCursorPos(1, 2)
|
||||
win.write(" ")
|
||||
win.setBackgroundColor(header_color)
|
||||
win.write(string.rep(" ", header_pad_x) .. header .. string.rep(" ", header_pad_x))
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.write(" ")
|
||||
|
||||
-- create strings for use in blit
|
||||
local line_text = string.rep(" ", 14)
|
||||
local line_text_color = string.rep("0", 14)
|
||||
|
||||
-- draw components --
|
||||
|
||||
-- draw reactor
|
||||
local rod = "88"
|
||||
if data.state.high_temp then
|
||||
rod = "11"
|
||||
elseif running then
|
||||
rod = "99"
|
||||
end
|
||||
|
||||
win.setCursorPos(4, 4)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
win.setCursorPos(4, 5)
|
||||
win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77")
|
||||
win.setCursorPos(4, 6)
|
||||
win.blit(line_text, line_text_color, "7777" .. rod .. "77" .. rod .. "7777")
|
||||
win.setCursorPos(4, 7)
|
||||
win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77")
|
||||
win.setCursorPos(4, 8)
|
||||
win.blit(line_text, line_text_color, "7777" .. rod .. "77" .. rod .. "7777")
|
||||
win.setCursorPos(4, 9)
|
||||
win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77")
|
||||
win.setCursorPos(4, 10)
|
||||
win.write(line_text)
|
||||
|
||||
-- boiler
|
||||
local steam = "ffffffffff"
|
||||
if running then
|
||||
steam = "0000000000"
|
||||
end
|
||||
|
||||
win.setCursorPos(4, 16)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
win.setCursorPos(4, 17)
|
||||
win.blit(line_text, line_text_color, "77" .. steam .. "77")
|
||||
win.setCursorPos(4, 18)
|
||||
win.blit(line_text, line_text_color, "77" .. steam .. "77")
|
||||
win.setCursorPos(4, 19)
|
||||
win.blit(line_text, line_text_color, "77888888888877")
|
||||
win.setCursorPos(4, 20)
|
||||
win.blit(line_text, line_text_color, "77bbbbbbbbbb77")
|
||||
win.setCursorPos(4, 21)
|
||||
win.blit(line_text, line_text_color, "77bbbbbbbbbb77")
|
||||
win.setCursorPos(4, 22)
|
||||
win.blit(line_text, line_text_color, "77bbbbbbbbbb77")
|
||||
win.setCursorPos(4, 23)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
|
||||
-- turbine
|
||||
win.setCursorPos(4, 29)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
win.setCursorPos(4, 30)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000000000077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ffffffffff77")
|
||||
end
|
||||
win.setCursorPos(4, 31)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77008000080077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ff8ffff8ff77")
|
||||
end
|
||||
win.setCursorPos(4, 32)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000800800077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77fff8ff8fff77")
|
||||
end
|
||||
win.setCursorPos(4, 33)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000088000077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ffff88ffff77")
|
||||
end
|
||||
win.setCursorPos(4, 34)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000800800077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77fff8ff8fff77")
|
||||
end
|
||||
win.setCursorPos(4, 35)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77008000080077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ff8ffff8ff77")
|
||||
end
|
||||
win.setCursorPos(4, 36)
|
||||
if running then
|
||||
win.blit(line_text, line_text_color, "77000000000077")
|
||||
else
|
||||
win.blit(line_text, line_text_color, "77ffffffffff77")
|
||||
end
|
||||
win.setCursorPos(4, 37)
|
||||
win.setBackgroundColor(colors.gray)
|
||||
win.write(line_text)
|
||||
|
||||
-- draw reactor coolant pipes
|
||||
draw_pipe(win, 7, 11, 6, colors.orange, colors.lightBlue)
|
||||
|
||||
-- draw turbine pipes
|
||||
draw_pipe(win, 7, 24, 6, colors.white, colors.blue)
|
||||
end
|
||||
|
||||
-- draw the reactor statuses on the status screen
|
||||
-- data: reactor table
|
||||
function draw_reactor_status(data)
|
||||
local win = data.render.win_stat
|
||||
|
||||
win.setBackgroundColor(colors.black)
|
||||
win.setTextColor(colors.white)
|
||||
win.clear()
|
||||
|
||||
-- show control state
|
||||
win.setCursorPos(1, 1)
|
||||
if data.control_state then
|
||||
win.blit(" + ENABLED", "00000000000", "dddffffffff")
|
||||
else
|
||||
win.blit(" - DISABLED", "000000000000", "eeefffffffff")
|
||||
end
|
||||
|
||||
-- show run state
|
||||
win.setCursorPos(1, 2)
|
||||
if data.state.run then
|
||||
win.blit(" + RUNNING", "00000000000", "dddffffffff")
|
||||
else
|
||||
win.blit(" - STOPPED", "00000000000", "888ffffffff")
|
||||
end
|
||||
|
||||
-- show fuel state
|
||||
win.setCursorPos(1, 4)
|
||||
if data.state.no_fuel then
|
||||
win.blit(" - NO FUEL", "00000000000", "eeeffffffff")
|
||||
else
|
||||
win.blit(" + FUEL OK", "00000000000", "999ffffffff")
|
||||
end
|
||||
|
||||
-- show waste state
|
||||
win.setCursorPos(1, 5)
|
||||
if data.state.full_waste then
|
||||
win.blit(" - WASTE FULL", "00000000000000", "eeefffffffffff")
|
||||
else
|
||||
win.blit(" + WASTE OK", "000000000000", "999fffffffff")
|
||||
end
|
||||
|
||||
-- show high temp state
|
||||
win.setCursorPos(1, 6)
|
||||
if data.state.high_temp then
|
||||
win.blit(" - HIGH TEMP", "0000000000000", "eeeffffffffff")
|
||||
else
|
||||
win.blit(" + TEMP OK", "00000000000", "999ffffffff")
|
||||
end
|
||||
|
||||
-- show damage state
|
||||
win.setCursorPos(1, 7)
|
||||
if data.state.damage_crit then
|
||||
win.blit(" - CRITICAL DAMAGE", "0000000000000000000", "eeeffffffffffffffff")
|
||||
else
|
||||
win.blit(" + CASING INTACT", "00000000000000000", "999ffffffffffffff")
|
||||
end
|
||||
|
||||
-- waste processing options --
|
||||
win.setTextColor(colors.black)
|
||||
win.setBackgroundColor(colors.white)
|
||||
|
||||
win.setCursorPos(1, 10)
|
||||
win.write(" ")
|
||||
win.setCursorPos(1, 11)
|
||||
win.write(" WASTE OUTPUT ")
|
||||
|
||||
win.setCursorPos(1, 13)
|
||||
win.setBackgroundColor(colors.cyan)
|
||||
if data.waste_production == "plutonium" then
|
||||
win.write(" > plutonium ")
|
||||
else
|
||||
win.write(" plutonium ")
|
||||
end
|
||||
|
||||
win.setCursorPos(1, 15)
|
||||
win.setBackgroundColor(colors.green)
|
||||
if data.waste_production == "polonium" then
|
||||
win.write(" > polonium ")
|
||||
else
|
||||
win.write(" polonium ")
|
||||
end
|
||||
|
||||
win.setCursorPos(1, 17)
|
||||
win.setBackgroundColor(colors.purple)
|
||||
if data.waste_production == "antimatter" then
|
||||
win.write(" > antimatter ")
|
||||
else
|
||||
win.write(" antimatter ")
|
||||
end
|
||||
end
|
||||
|
||||
-- update the system monitor screen
|
||||
-- mon: monitor to update
|
||||
-- is_scrammed:
|
||||
function update_system_monitor(mon, is_scrammed, reactors)
|
||||
if is_scrammed then
|
||||
-- display scram banner
|
||||
mon.setTextColor(colors.white)
|
||||
mon.setBackgroundColor(colors.black)
|
||||
mon.setCursorPos(1, 2)
|
||||
mon.clearLine()
|
||||
mon.setBackgroundColor(colors.red)
|
||||
mon.setCursorPos(1, 3)
|
||||
mon.write(" ")
|
||||
mon.setCursorPos(1, 4)
|
||||
mon.write(" SCRAM ")
|
||||
mon.setCursorPos(1, 5)
|
||||
mon.write(" ")
|
||||
mon.setBackgroundColor(colors.black)
|
||||
mon.setCursorPos(1, 6)
|
||||
mon.clearLine()
|
||||
mon.setTextColor(colors.white)
|
||||
else
|
||||
-- clear where scram banner would be
|
||||
mon.setCursorPos(1, 3)
|
||||
mon.clearLine()
|
||||
mon.setCursorPos(1, 4)
|
||||
mon.clearLine()
|
||||
mon.setCursorPos(1, 5)
|
||||
mon.clearLine()
|
||||
|
||||
-- show production statistics--
|
||||
|
||||
local mrf_t = 0
|
||||
local mb_t = 0
|
||||
local plutonium = 0
|
||||
local polonium = 0
|
||||
local spent_waste = 0
|
||||
local antimatter = 0
|
||||
|
||||
-- determine production values
|
||||
for key, rctr in pairs(reactors) do
|
||||
if rctr.state.run then
|
||||
mrf_t = mrf_t + defs.TURBINE_MRF_T
|
||||
mb_t = mb_t + defs.REACTOR_MB_T
|
||||
|
||||
if rctr.waste_production == "plutonium" then
|
||||
plutonium = plutonium + (defs.REACTOR_MB_T * defs.PLUTONIUM_PER_WASTE)
|
||||
spent_waste = spent_waste + (defs.REACTOR_MB_T * defs.PLUTONIUM_PER_WASTE * defs.SPENT_PER_BYPRODUCT)
|
||||
elseif rctr.waste_production == "polonium" then
|
||||
polonium = polonium + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE)
|
||||
spent_waste = spent_waste + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE * defs.SPENT_PER_BYPRODUCT)
|
||||
elseif rctr.waste_production == "antimatter" then
|
||||
antimatter = antimatter + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE * defs.ANTIMATTER_PER_POLONIUM)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw stats
|
||||
mon.setTextColor(colors.lightGray)
|
||||
mon.setCursorPos(1, 2)
|
||||
mon.clearLine()
|
||||
mon.write("ENERGY: " .. string.format("%0.2f", mrf_t) .. " MRF/t")
|
||||
-- mon.setCursorPos(1, 3)
|
||||
-- mon.clearLine()
|
||||
-- mon.write("FUEL: " .. mb_t .. " mB/t")
|
||||
mon.setCursorPos(1, 3)
|
||||
mon.clearLine()
|
||||
mon.write("Pu: " .. string.format("%0.2f", plutonium) .. " mB/t")
|
||||
mon.setCursorPos(1, 4)
|
||||
mon.clearLine()
|
||||
mon.write("Po: " .. string.format("%0.2f", polonium) .. " mB/t")
|
||||
mon.setCursorPos(1, 5)
|
||||
mon.clearLine()
|
||||
mon.write("SPENT: " .. string.format("%0.2f", spent_waste) .. " mB/t")
|
||||
mon.setCursorPos(1, 6)
|
||||
mon.clearLine()
|
||||
mon.write("ANTI-M: " .. string.format("%0.2f", antimatter * 1000) .. " uB/t")
|
||||
mon.setTextColor(colors.white)
|
||||
end
|
||||
end
|
||||
109
main/server.lua
109
main/server.lua
@@ -1,109 +0,0 @@
|
||||
os.loadAPI("defs.lua")
|
||||
os.loadAPI("log.lua")
|
||||
os.loadAPI("regulator.lua")
|
||||
|
||||
local modem
|
||||
local reactors
|
||||
|
||||
-- initalize the listener running on the wireless modem
|
||||
-- _reactors: reactor table
|
||||
function init(_reactors)
|
||||
modem = peripheral.wrap("top")
|
||||
reactors = _reactors
|
||||
|
||||
-- open listening port
|
||||
if not modem.isOpen(defs.LISTEN_PORT) then
|
||||
modem.open(defs.LISTEN_PORT)
|
||||
end
|
||||
|
||||
-- send out a greeting to solicit responses for clients that are already running
|
||||
broadcast(0, reactors)
|
||||
end
|
||||
|
||||
-- handle an incoming message from the modem
|
||||
-- packet: table containing message fields
|
||||
function handle_message(packet)
|
||||
if type(packet.message) == "number" then
|
||||
-- this is a greeting
|
||||
log.write("reactor " .. packet.message .. " connected", colors.green)
|
||||
|
||||
-- send current control command
|
||||
for key, rctr in pairs(reactors) do
|
||||
if rctr.id == packet.message then
|
||||
send(rctr.id, rctr.control_state)
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
-- got reactor status
|
||||
local eval_safety = false
|
||||
|
||||
for key, value in pairs(reactors) do
|
||||
if value.id == packet.message.id then
|
||||
local tag = "RCT-" .. value.id .. ": "
|
||||
|
||||
if value.state.run ~= packet.message.run then
|
||||
value.state.run = packet.message.run
|
||||
if value.state.run then
|
||||
eval_safety = true
|
||||
log.write(tag .. "running", colors.green)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.no_fuel ~= packet.message.no_fuel then
|
||||
value.state.no_fuel = packet.message.no_fuel
|
||||
if value.state.no_fuel then
|
||||
eval_safety = true
|
||||
log.write(tag .. "insufficient fuel", colors.gray)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.full_waste ~= packet.message.full_waste then
|
||||
value.state.full_waste = packet.message.full_waste
|
||||
if value.state.full_waste then
|
||||
eval_safety = true
|
||||
log.write(tag .. "waste tank full", colors.brown)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.high_temp ~= packet.message.high_temp then
|
||||
value.state.high_temp = packet.message.high_temp
|
||||
if value.state.high_temp then
|
||||
eval_safety = true
|
||||
log.write(tag .. "high temperature", colors.orange)
|
||||
end
|
||||
end
|
||||
|
||||
if value.state.damage_crit ~= packet.message.damage_crit then
|
||||
value.state.damage_crit = packet.message.damage_crit
|
||||
if value.state.damage_crit then
|
||||
eval_safety = true
|
||||
log.write(tag .. "critical damage", colors.red)
|
||||
end
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- check to ensure safe operation
|
||||
if eval_safety then
|
||||
regulator.enforce_safeties()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- send a message to a given reactor
|
||||
-- dest: reactor ID
|
||||
-- message: true or false for enable control or another value for other functionality, like 0 for greeting
|
||||
function send(dest, message)
|
||||
modem.transmit(dest + defs.LISTEN_PORT, defs.LISTEN_PORT, message)
|
||||
end
|
||||
|
||||
-- broadcast a message to all reactors
|
||||
-- message: true or false for enable control or another value for other functionality, like 0 for greeting
|
||||
function broadcast(message)
|
||||
for key, value in pairs(reactors) do
|
||||
modem.transmit(value.id + defs.LISTEN_PORT, defs.LISTEN_PORT, message)
|
||||
end
|
||||
end
|
||||
0
pocket/config.lua
Normal file
0
pocket/config.lua
Normal file
5
pocket/startup.lua
Normal file
5
pocket/startup.lua
Normal file
@@ -0,0 +1,5 @@
|
||||
--
|
||||
-- SCADA Coordinator Access on a Pocket Computer
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
18
reactor-plc/config.lua
Normal file
18
reactor-plc/config.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local config = {}
|
||||
|
||||
-- set to false to run in offline mode (safety regulation only)
|
||||
config.NETWORKED = true
|
||||
-- unique reactor ID
|
||||
config.REACTOR_ID = 1
|
||||
-- port to send packets TO server
|
||||
config.SERVER_PORT = 16000
|
||||
-- port to listen to incoming packets FROM server
|
||||
config.LISTEN_PORT = 14001
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
|
||||
return config
|
||||
783
reactor-plc/plc.lua
Normal file
783
reactor-plc/plc.lua
Normal file
@@ -0,0 +1,783 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local plc = {}
|
||||
|
||||
local rps_status_t = types.rps_status_t
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local RPLC_TYPES = comms.RPLC_TYPES
|
||||
local RPLC_LINKING = comms.RPLC_LINKING
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
--- RPS: Reactor Protection System
|
||||
---
|
||||
--- identifies dangerous states and SCRAMs reactor if warranted
|
||||
---
|
||||
--- autonomous from main SCADA supervisor/coordinator control
|
||||
function plc.rps_init(reactor)
|
||||
local state_keys = {
|
||||
dmg_crit = 1,
|
||||
high_temp = 2,
|
||||
no_coolant = 3,
|
||||
ex_waste = 4,
|
||||
ex_hcoolant = 5,
|
||||
no_fuel = 6,
|
||||
fault = 7,
|
||||
timeout = 8,
|
||||
manual = 9
|
||||
}
|
||||
|
||||
local self = {
|
||||
reactor = reactor,
|
||||
state = { false, false, false, false, false, false, false, false, false },
|
||||
reactor_enabled = false,
|
||||
tripped = false,
|
||||
trip_cause = "" ---@type rps_trip_cause
|
||||
}
|
||||
|
||||
---@class rps
|
||||
local public = {}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- set reactor access fault flag
|
||||
local function _set_fault()
|
||||
if self.reactor.__p_last_fault() ~= "Terminated" then
|
||||
self.state[state_keys.fault] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- clear reactor access fault flag
|
||||
local function _clear_fault()
|
||||
self.state[state_keys.fault] = false
|
||||
end
|
||||
|
||||
-- check for critical damage
|
||||
local function _damage_critical()
|
||||
local damage_percent = self.reactor.getDamagePercent()
|
||||
if damage_percent == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
log.error("RPS: failed to check reactor damage")
|
||||
_set_fault()
|
||||
self.state[state_keys.dmg_crit] = false
|
||||
else
|
||||
self.state[state_keys.dmg_crit] = damage_percent >= 100
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the reactor is at a critically high temperature
|
||||
local function _high_temp()
|
||||
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
|
||||
local temp = self.reactor.getTemperature()
|
||||
if temp == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
log.error("RPS: failed to check reactor temperature")
|
||||
_set_fault()
|
||||
self.state[state_keys.high_temp] = false
|
||||
else
|
||||
self.state[state_keys.high_temp] = temp >= 1200
|
||||
end
|
||||
end
|
||||
|
||||
-- check if there is no coolant (<2% filled)
|
||||
local function _no_coolant()
|
||||
local coolant_filled = self.reactor.getCoolantFilledPercentage()
|
||||
if coolant_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
log.error("RPS: failed to check reactor coolant level")
|
||||
_set_fault()
|
||||
self.state[state_keys.no_coolant] = false
|
||||
else
|
||||
self.state[state_keys.no_coolant] = coolant_filled < 0.02
|
||||
end
|
||||
end
|
||||
|
||||
-- check for excess waste (>80% filled)
|
||||
local function _excess_waste()
|
||||
local w_filled = self.reactor.getWasteFilledPercentage()
|
||||
if w_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
log.error("RPS: failed to check reactor waste level")
|
||||
_set_fault()
|
||||
self.state[state_keys.ex_waste] = false
|
||||
else
|
||||
self.state[state_keys.ex_waste] = w_filled > 0.8
|
||||
end
|
||||
end
|
||||
|
||||
-- check for heated coolant backup (>95% filled)
|
||||
local function _excess_heated_coolant()
|
||||
local hc_filled = self.reactor.getHeatedCoolantFilledPercentage()
|
||||
if hc_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
log.error("RPS: failed to check reactor heated coolant level")
|
||||
_set_fault()
|
||||
self.state[state_keys.ex_hcoolant] = false
|
||||
else
|
||||
self.state[state_keys.ex_hcoolant] = hc_filled > 0.95
|
||||
end
|
||||
end
|
||||
|
||||
-- check if there is no fuel
|
||||
local function _insufficient_fuel()
|
||||
local fuel = self.reactor.getFuel()
|
||||
if fuel == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
log.error("RPS: failed to check reactor fuel")
|
||||
_set_fault()
|
||||
self.state[state_keys.no_fuel] = false
|
||||
else
|
||||
self.state[state_keys.no_fuel] = fuel == 0
|
||||
end
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- re-link a reactor after a peripheral re-connect
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_reactor(reactor)
|
||||
self.reactor = reactor
|
||||
end
|
||||
|
||||
-- trip for lost peripheral
|
||||
function public.trip_fault()
|
||||
_set_fault()
|
||||
end
|
||||
|
||||
-- trip for a PLC comms timeout
|
||||
function public.trip_timeout()
|
||||
self.state[state_keys.timeout] = true
|
||||
end
|
||||
|
||||
-- manually SCRAM the reactor
|
||||
function public.trip_manual()
|
||||
self.state[state_keys.manual] = true
|
||||
end
|
||||
|
||||
-- SCRAM the reactor now
|
||||
---@return boolean success
|
||||
function public.scram()
|
||||
log.info("RPS: reactor SCRAM")
|
||||
|
||||
self.reactor.scram()
|
||||
if self.reactor.__p_is_faulted() then
|
||||
log.error("RPS: failed reactor SCRAM")
|
||||
return false
|
||||
else
|
||||
self.reactor_enabled = false
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- start the reactor
|
||||
---@return boolean success
|
||||
function public.activate()
|
||||
if not self.tripped then
|
||||
log.info("RPS: reactor start")
|
||||
|
||||
self.reactor.activate()
|
||||
if self.reactor.__p_is_faulted() then
|
||||
log.error("RPS: failed reactor start")
|
||||
else
|
||||
self.reactor_enabled = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- check all safety conditions
|
||||
---@return boolean tripped, rps_status_t trip_status, boolean first_trip
|
||||
function public.check()
|
||||
local status = rps_status_t.ok
|
||||
local was_tripped = self.tripped
|
||||
local first_trip = false
|
||||
|
||||
-- update state
|
||||
parallel.waitForAll(
|
||||
_damage_critical,
|
||||
_high_temp,
|
||||
_no_coolant,
|
||||
_excess_waste,
|
||||
_excess_heated_coolant,
|
||||
_insufficient_fuel
|
||||
)
|
||||
|
||||
-- check system states in order of severity
|
||||
if self.tripped then
|
||||
status = self.trip_cause
|
||||
elseif self.state[state_keys.dmg_crit] then
|
||||
log.warning("RPS: damage critical")
|
||||
status = rps_status_t.dmg_crit
|
||||
elseif self.state[state_keys.high_temp] then
|
||||
log.warning("RPS: high temperature")
|
||||
status = rps_status_t.high_temp
|
||||
elseif self.state[state_keys.no_coolant] then
|
||||
log.warning("RPS: no coolant")
|
||||
status = rps_status_t.no_coolant
|
||||
elseif self.state[state_keys.ex_waste] then
|
||||
log.warning("RPS: full waste")
|
||||
status = rps_status_t.ex_waste
|
||||
elseif self.state[state_keys.ex_hcoolant] then
|
||||
log.warning("RPS: heated coolant backup")
|
||||
status = rps_status_t.ex_hcoolant
|
||||
elseif self.state[state_keys.no_fuel] then
|
||||
log.warning("RPS: no fuel")
|
||||
status = rps_status_t.no_fuel
|
||||
elseif self.state[state_keys.fault] then
|
||||
log.warning("RPS: reactor access fault")
|
||||
status = rps_status_t.fault
|
||||
elseif self.state[state_keys.timeout] then
|
||||
log.warning("RPS: supervisor connection timeout")
|
||||
status = rps_status_t.timeout
|
||||
elseif self.state[state_keys.manual] then
|
||||
log.warning("RPS: manual SCRAM requested")
|
||||
status = rps_status_t.manual
|
||||
else
|
||||
self.tripped = false
|
||||
end
|
||||
|
||||
-- if a new trip occured...
|
||||
if (not was_tripped) and (status ~= rps_status_t.ok) then
|
||||
first_trip = true
|
||||
self.tripped = true
|
||||
self.trip_cause = status
|
||||
|
||||
public.scram()
|
||||
end
|
||||
|
||||
return self.tripped, status, first_trip
|
||||
end
|
||||
|
||||
function public.status() return self.state end
|
||||
function public.is_tripped() return self.tripped end
|
||||
function public.is_active() return self.reactor_enabled end
|
||||
|
||||
-- reset the RPS
|
||||
function public.reset()
|
||||
self.tripped = false
|
||||
self.trip_cause = rps_status_t.ok
|
||||
|
||||
for i = 1, #self.state do
|
||||
self.state[i] = false
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- Reactor PLC Communications
|
||||
---@param id integer
|
||||
---@param version string
|
||||
---@param modem table
|
||||
---@param local_port integer
|
||||
---@param server_port integer
|
||||
---@param reactor table
|
||||
---@param rps rps
|
||||
---@param conn_watchdog watchdog
|
||||
function plc.comms(id, version, modem, local_port, server_port, reactor, rps, conn_watchdog)
|
||||
local self = {
|
||||
id = id,
|
||||
version = version,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
modem = modem,
|
||||
s_port = server_port,
|
||||
l_port = local_port,
|
||||
reactor = reactor,
|
||||
rps = rps,
|
||||
conn_watchdog = conn_watchdog,
|
||||
scrammed = false,
|
||||
linked = false,
|
||||
status_cache = nil,
|
||||
max_burn_rate = nil
|
||||
}
|
||||
|
||||
---@class plc_comms
|
||||
local public = {}
|
||||
|
||||
-- open modem
|
||||
if not self.modem.isOpen(self.l_port) then
|
||||
self.modem.open(self.l_port)
|
||||
end
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- send an RPLC packet
|
||||
---@param msg_type RPLC_TYPES
|
||||
---@param msg string
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local r_pkt = comms.rplc_packet()
|
||||
|
||||
r_pkt.make(self.id, msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg string
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- variable reactor status information, excluding heating rate
|
||||
---@return table data_table, boolean faulted
|
||||
local function _reactor_status()
|
||||
local fuel = nil
|
||||
local waste = nil
|
||||
local coolant = nil
|
||||
local hcoolant = nil
|
||||
|
||||
local data_table = {
|
||||
false, -- getStatus
|
||||
0, -- getBurnRate
|
||||
0, -- getActualBurnRate
|
||||
0, -- getTemperature
|
||||
0, -- getDamagePercent
|
||||
0, -- getBoilEfficiency
|
||||
0, -- getEnvironmentalLoss
|
||||
0, -- getFuel
|
||||
0, -- getFuelFilledPercentage
|
||||
0, -- getWaste
|
||||
0, -- getWasteFilledPercentage
|
||||
"", -- coolant_name
|
||||
0, -- coolant_amnt
|
||||
0, -- getCoolantFilledPercentage
|
||||
"", -- hcoolant_name
|
||||
0, -- hcoolant_amnt
|
||||
0 -- getHeatedCoolantFilledPercentage
|
||||
}
|
||||
|
||||
local tasks = {
|
||||
function () data_table[1] = self.reactor.getStatus() end,
|
||||
function () data_table[2] = self.reactor.getBurnRate() end,
|
||||
function () data_table[3] = self.reactor.getActualBurnRate() end,
|
||||
function () data_table[4] = self.reactor.getTemperature() end,
|
||||
function () data_table[5] = self.reactor.getDamagePercent() end,
|
||||
function () data_table[6] = self.reactor.getBoilEfficiency() end,
|
||||
function () data_table[7] = self.reactor.getEnvironmentalLoss() end,
|
||||
function () fuel = self.reactor.getFuel() end,
|
||||
function () data_table[9] = self.reactor.getFuelFilledPercentage() end,
|
||||
function () waste = self.reactor.getWaste() end,
|
||||
function () data_table[11] = self.reactor.getWasteFilledPercentage() end,
|
||||
function () coolant = self.reactor.getCoolant() end,
|
||||
function () data_table[14] = self.reactor.getCoolantFilledPercentage() end,
|
||||
function () hcoolant = self.reactor.getHeatedCoolant() end,
|
||||
function () data_table[17] = self.reactor.getHeatedCoolantFilledPercentage() end
|
||||
}
|
||||
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if type(fuel) == "table" then
|
||||
data_table[8] = fuel.amount
|
||||
elseif type(fuel) == "number" then
|
||||
data_table[8] = fuel
|
||||
end
|
||||
|
||||
if type(waste) == "table" then
|
||||
data_table[10] = waste.amount
|
||||
elseif type(waste) == "number" then
|
||||
data_table[10] = waste
|
||||
end
|
||||
|
||||
if coolant ~= nil then
|
||||
data_table[12] = coolant.name
|
||||
data_table[13] = coolant.amount
|
||||
end
|
||||
|
||||
if hcoolant ~= nil then
|
||||
data_table[15] = hcoolant.name
|
||||
data_table[16] = hcoolant.amount
|
||||
end
|
||||
|
||||
return data_table, self.reactor.__p_is_faulted()
|
||||
end
|
||||
|
||||
-- update the status cache if changed
|
||||
---@return boolean changed
|
||||
local function _update_status_cache()
|
||||
local status, faulted = _reactor_status()
|
||||
local changed = false
|
||||
|
||||
if self.status_cache ~= nil then
|
||||
if not faulted then
|
||||
for i = 1, #status do
|
||||
if status[i] ~= self.status_cache[i] then
|
||||
changed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
changed = true
|
||||
end
|
||||
|
||||
if changed and not faulted then
|
||||
self.status_cache = status
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- general ack
|
||||
---@param msg_type RPLC_TYPES
|
||||
---@param succeeded boolean
|
||||
local function _send_ack(msg_type, succeeded)
|
||||
_send(msg_type, { succeeded })
|
||||
end
|
||||
|
||||
-- send structure properties (these should not change, server will cache these)
|
||||
local function _send_struct()
|
||||
local mek_data = { 0, 0, 0, 0, 0, 0, 0, 0 }
|
||||
|
||||
local tasks = {
|
||||
function () mek_data[1] = self.reactor.getHeatCapacity() end,
|
||||
function () mek_data[2] = self.reactor.getFuelAssemblies() end,
|
||||
function () mek_data[3] = self.reactor.getFuelSurfaceArea() end,
|
||||
function () mek_data[4] = self.reactor.getFuelCapacity() end,
|
||||
function () mek_data[5] = self.reactor.getWasteCapacity() end,
|
||||
function () mek_data[6] = self.reactor.getCoolantCapacity() end,
|
||||
function () mek_data[7] = self.reactor.getHeatedCoolantCapacity() end,
|
||||
function () mek_data[8] = self.reactor.getMaxBurnRate() end
|
||||
}
|
||||
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if not self.reactor.__p_is_faulted() then
|
||||
_send(RPLC_TYPES.MEK_STRUCT, mek_data)
|
||||
else
|
||||
log.error("failed to send structure: PPM fault")
|
||||
end
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- reconnect a newly connected modem
|
||||
---@param modem table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_modem(modem)
|
||||
self.modem = modem
|
||||
|
||||
-- open modem
|
||||
if not self.modem.isOpen(self.l_port) then
|
||||
self.modem.open(self.l_port)
|
||||
end
|
||||
end
|
||||
|
||||
-- reconnect a newly connected reactor
|
||||
---@param reactor table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_reactor(reactor)
|
||||
self.reactor = reactor
|
||||
self.status_cache = nil
|
||||
end
|
||||
|
||||
-- unlink from the server
|
||||
function public.unlink()
|
||||
self.linked = false
|
||||
self.r_seq_num = nil
|
||||
self.status_cache = nil
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
self.conn_watchdog.cancel()
|
||||
public.unlink()
|
||||
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
end
|
||||
|
||||
-- attempt to establish link with supervisor
|
||||
function public.send_link_req()
|
||||
_send(RPLC_TYPES.LINK_REQ, { self.id, self.version })
|
||||
end
|
||||
|
||||
-- send live status information
|
||||
---@param degraded boolean
|
||||
function public.send_status(degraded)
|
||||
if self.linked then
|
||||
local mek_data = nil
|
||||
|
||||
if _update_status_cache() then
|
||||
mek_data = self.status_cache
|
||||
end
|
||||
|
||||
local sys_status = {
|
||||
util.time(), -- timestamp
|
||||
(not self.scrammed), -- requested control state
|
||||
rps.is_tripped(), -- rps_tripped
|
||||
degraded, -- degraded
|
||||
self.reactor.getHeatingRate(), -- heating rate
|
||||
mek_data -- mekanism status data
|
||||
}
|
||||
|
||||
if not self.reactor.__p_is_faulted() then
|
||||
_send(RPLC_TYPES.STATUS, sys_status)
|
||||
else
|
||||
log.error("failed to send status: PPM fault")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- send reactor protection system status
|
||||
function public.send_rps_status()
|
||||
if self.linked then
|
||||
_send(RPLC_TYPES.RPS_STATUS, rps.status())
|
||||
end
|
||||
end
|
||||
|
||||
-- send reactor protection system alarm
|
||||
---@param cause rps_status_t
|
||||
function public.send_rps_alarm(cause)
|
||||
if self.linked then
|
||||
local rps_alarm = {
|
||||
cause,
|
||||
table.unpack(rps.status())
|
||||
}
|
||||
|
||||
_send(RPLC_TYPES.RPS_ALARM, rps_alarm)
|
||||
end
|
||||
end
|
||||
|
||||
-- parse an RPLC packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return rplc_frame|mgmt_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local s_pkt = comms.scada_packet()
|
||||
|
||||
-- parse packet as generic SCADA packet
|
||||
s_pkt.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as RPLC packet
|
||||
if s_pkt.protocol() == PROTOCOLS.RPLC then
|
||||
local rplc_pkt = comms.rplc_packet()
|
||||
if rplc_pkt.decode(s_pkt) then
|
||||
pkt = rplc_pkt.get()
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
else
|
||||
log.error("illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle an RPLC packet
|
||||
---@param packet rplc_frame|mgmt_frame
|
||||
---@param plc_state plc_state
|
||||
---@param setpoints setpoints
|
||||
function public.handle_packet(packet, plc_state, setpoints)
|
||||
if packet ~= nil then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.linked and self.r_seq_num >= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
|
||||
-- feed the watchdog first so it doesn't uhh...eat our packets :)
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOLS.RPLC then
|
||||
if self.linked then
|
||||
if packet.type == RPLC_TYPES.LINK_REQ then
|
||||
-- link request confirmation
|
||||
if packet.length == 1 then
|
||||
log.debug("received unsolicited link request response")
|
||||
|
||||
local link_ack = packet.data[1]
|
||||
|
||||
if link_ack == RPLC_LINKING.ALLOW then
|
||||
self.status_cache = nil
|
||||
_send_struct()
|
||||
public.send_status(plc_state.degraded)
|
||||
log.debug("re-sent initial status data")
|
||||
elseif link_ack == RPLC_LINKING.DENY then
|
||||
println_ts("received unsolicited link denial, unlinking")
|
||||
log.debug("unsolicited RPLC link request denied")
|
||||
elseif link_ack == RPLC_LINKING.COLLISION then
|
||||
println_ts("received unsolicited link collision, unlinking")
|
||||
log.warning("unsolicited RPLC link request collision")
|
||||
else
|
||||
println_ts("invalid unsolicited link response")
|
||||
log.error("unsolicited unknown RPLC link request response")
|
||||
end
|
||||
|
||||
self.linked = link_ack == RPLC_LINKING.ALLOW
|
||||
else
|
||||
log.debug("RPLC link req packet length mismatch")
|
||||
end
|
||||
elseif packet.type == RPLC_TYPES.STATUS then
|
||||
-- request of full status, clear cache first
|
||||
self.status_cache = nil
|
||||
public.send_status(plc_state.degraded)
|
||||
log.debug("sent out status cache again, did supervisor miss it?")
|
||||
elseif packet.type == RPLC_TYPES.MEK_STRUCT then
|
||||
-- request for physical structure
|
||||
_send_struct()
|
||||
log.debug("sent out structure again, did supervisor miss it?")
|
||||
elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then
|
||||
-- set the burn rate
|
||||
if packet.length == 2 then
|
||||
local success = false
|
||||
local burn_rate = packet.data[1]
|
||||
local ramp = packet.data[2]
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = self.reactor.getMaxBurnRate()
|
||||
end
|
||||
|
||||
-- if we know our max burn rate, update current burn rate setpoint if in range
|
||||
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
|
||||
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
|
||||
if ramp then
|
||||
setpoints.burn_rate_en = true
|
||||
setpoints.burn_rate = burn_rate
|
||||
success = true
|
||||
else
|
||||
self.reactor.setBurnRate(burn_rate)
|
||||
success = not self.reactor.__p_is_faulted()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
_send_ack(packet.type, success)
|
||||
else
|
||||
log.debug("RPLC set burn rate packet length mismatch")
|
||||
end
|
||||
elseif packet.type == RPLC_TYPES.RPS_ENABLE then
|
||||
-- enable the reactor
|
||||
self.scrammed = false
|
||||
_send_ack(packet.type, self.rps.activate())
|
||||
elseif packet.type == RPLC_TYPES.RPS_SCRAM then
|
||||
-- disable the reactor
|
||||
self.scrammed = true
|
||||
self.rps.trip_manual()
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPES.RPS_RESET then
|
||||
-- reset the RPS status
|
||||
rps.reset()
|
||||
_send_ack(packet.type, true)
|
||||
else
|
||||
log.warning("received unknown RPLC packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == RPLC_TYPES.LINK_REQ then
|
||||
-- link request confirmation
|
||||
if packet.length == 1 then
|
||||
local link_ack = packet.data[1]
|
||||
|
||||
if link_ack == RPLC_LINKING.ALLOW then
|
||||
println_ts("linked!")
|
||||
log.debug("RPLC link request approved")
|
||||
|
||||
-- reset remote sequence number and cache
|
||||
self.r_seq_num = nil
|
||||
self.status_cache = nil
|
||||
|
||||
_send_struct()
|
||||
public.send_status(plc_state.degraded)
|
||||
|
||||
log.debug("sent initial status data")
|
||||
elseif link_ack == RPLC_LINKING.DENY then
|
||||
println_ts("link request denied, retrying...")
|
||||
log.debug("RPLC link request denied")
|
||||
elseif link_ack == RPLC_LINKING.COLLISION then
|
||||
println_ts("reactor PLC ID collision (check config), retrying...")
|
||||
log.warning("RPLC link request collision")
|
||||
else
|
||||
println_ts("invalid link response, bad channel? retrying...")
|
||||
log.error("unknown RPLC link request response")
|
||||
end
|
||||
|
||||
self.linked = link_ack == RPLC_LINKING.ALLOW
|
||||
else
|
||||
log.debug("RPLC link req packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("discarding non-link packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 500 then
|
||||
log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("RPLC RTT = " .. trip_time .. "ms")
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
-- handle session close
|
||||
self.conn_watchdog.cancel()
|
||||
public.unlink()
|
||||
println_ts("server connection closed by remote host")
|
||||
log.warning("server connection closed by remote host")
|
||||
else
|
||||
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
else
|
||||
-- should be unreachable assuming packet is from parse_packet()
|
||||
log.error("illegal packet type " .. protocol, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function public.is_scrammed() return self.scrammed end
|
||||
function public.is_linked() return self.linked end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return plc
|
||||
197
reactor-plc/startup.lua
Normal file
197
reactor-plc/startup.lua
Normal file
@@ -0,0 +1,197 @@
|
||||
--
|
||||
-- Reactor Programmable Logic Controller
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("reactor-plc.config")
|
||||
local plc = require("reactor-plc.plc")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "beta-v0.8.2"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_bool(config.NETWORKED)
|
||||
cfv.assert_type_int(config.REACTOR_ID)
|
||||
cfv.assert_port(config.SERVER_PORT)
|
||||
cfv.assert_port(config.LISTEN_PORT)
|
||||
cfv.assert_type_str(config.LOG_PATH)
|
||||
cfv.assert_type_int(config.LOG_MODE)
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
|
||||
log.info("========================================")
|
||||
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
|
||||
|
||||
----------------------------------------
|
||||
-- startup
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
-- shared memory across threads
|
||||
---@class plc_shared_memory
|
||||
local __shared_memory = {
|
||||
-- networked setting
|
||||
networked = config.NETWORKED, ---@type boolean
|
||||
|
||||
-- PLC system state flags
|
||||
---@class plc_state
|
||||
plc_state = {
|
||||
init_ok = true,
|
||||
shutdown = false,
|
||||
degraded = false,
|
||||
no_reactor = false,
|
||||
no_modem = false
|
||||
},
|
||||
|
||||
-- control setpoints
|
||||
---@class setpoints
|
||||
setpoints = {
|
||||
burn_rate_en = false,
|
||||
burn_rate = 0.0
|
||||
},
|
||||
|
||||
-- core PLC devices
|
||||
plc_dev = {
|
||||
reactor = ppm.get_fission_reactor(),
|
||||
modem = ppm.get_wireless_modem()
|
||||
},
|
||||
|
||||
-- system objects
|
||||
plc_sys = {
|
||||
rps = nil, ---@type rps
|
||||
plc_comms = nil, ---@type plc_comms
|
||||
conn_watchdog = nil ---@type watchdog
|
||||
},
|
||||
|
||||
-- message queues
|
||||
q = {
|
||||
mq_rps = mqueue.new(),
|
||||
mq_comms_tx = mqueue.new(),
|
||||
mq_comms_rx = mqueue.new()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.plc_dev
|
||||
local smem_sys = __shared_memory.plc_sys
|
||||
|
||||
local plc_state = __shared_memory.plc_state
|
||||
|
||||
-- we need a reactor and a modem
|
||||
if smem_dev.reactor == nil then
|
||||
println("boot> fission reactor not found");
|
||||
log.warning("no reactor on startup")
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
plc_state.no_reactor = true
|
||||
end
|
||||
if __shared_memory.networked and smem_dev.modem == nil then
|
||||
println("boot> wireless modem not found")
|
||||
log.warning("no wireless modem on startup")
|
||||
|
||||
if smem_dev.reactor ~= nil then
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
plc_state.no_modem = true
|
||||
end
|
||||
|
||||
-- PLC init
|
||||
local function init()
|
||||
if plc_state.init_ok then
|
||||
-- just booting up, no fission allowed (neutrons stay put thanks)
|
||||
smem_dev.reactor.scram()
|
||||
|
||||
-- init reactor protection system
|
||||
smem_sys.rps = plc.rps_init(smem_dev.reactor)
|
||||
log.debug("init> rps init")
|
||||
|
||||
if __shared_memory.networked then
|
||||
-- comms watchdog, 3 second timeout
|
||||
smem_sys.conn_watchdog = util.new_watchdog(3)
|
||||
log.debug("init> conn watchdog started")
|
||||
|
||||
-- start comms
|
||||
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT,
|
||||
smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
|
||||
log.debug("init> comms init")
|
||||
else
|
||||
println("boot> starting in offline mode");
|
||||
log.debug("init> running without networking")
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
os.queueEvent("clock_start")
|
||||
|
||||
println("boot> completed");
|
||||
log.debug("init> boot completed")
|
||||
else
|
||||
println("boot> system in degraded state, awaiting devices...")
|
||||
log.warning("init> booted in a degraded state, awaiting peripheral connections...")
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
-- initialize PLC
|
||||
init()
|
||||
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory, init)
|
||||
local rps_thread = threads.thread__rps(__shared_memory)
|
||||
|
||||
if __shared_memory.networked then
|
||||
-- init comms threads
|
||||
local comms_thread_tx = threads.thread__comms_tx(__shared_memory)
|
||||
local comms_thread_rx = threads.thread__comms_rx(__shared_memory)
|
||||
|
||||
-- setpoint control only needed when networked
|
||||
local sp_ctrl_thread = threads.thread__setpoint_control(__shared_memory)
|
||||
|
||||
-- run threads
|
||||
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec)
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- send status one last time after RPS shutdown
|
||||
smem_sys.plc_comms.send_status(plc_state.degraded)
|
||||
smem_sys.plc_comms.send_rps_status()
|
||||
|
||||
-- close connection
|
||||
smem_sys.plc_comms.close()
|
||||
end
|
||||
else
|
||||
-- run threads, excluding comms
|
||||
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
|
||||
end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
626
reactor-plc/threads.lua
Normal file
626
reactor-plc/threads.lua
Normal file
@@ -0,0 +1,626 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local MAIN_CLOCK = 1 -- (1Hz, 20 ticks)
|
||||
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
|
||||
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
|
||||
local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks)
|
||||
|
||||
local BURN_RATE_RAMP_mB_s = 5.0
|
||||
|
||||
local MQ__RPS_CMD = {
|
||||
SCRAM = 1,
|
||||
DEGRADED_SCRAM = 2,
|
||||
TRIP_TIMEOUT = 3
|
||||
}
|
||||
|
||||
local MQ__COMM_CMD = {
|
||||
SEND_STATUS = 1
|
||||
}
|
||||
|
||||
-- main thread
|
||||
---@param smem plc_shared_memory
|
||||
---@param init function
|
||||
function threads.thread__main(smem, init)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("main thread init, clock inactive")
|
||||
|
||||
-- send status updates at 1Hz (every 20 server ticks) (every loop tick)
|
||||
-- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks)
|
||||
local LINK_TICKS = 4
|
||||
local ticks_to_update = 0
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- load in from shared memory
|
||||
local networked = smem.networked
|
||||
local plc_state = smem.plc_state
|
||||
local plc_dev = smem.plc_dev
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
local conn_watchdog = smem.plc_sys.conn_watchdog
|
||||
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" and loop_clock.is_clock(param1) then
|
||||
-- core clock tick
|
||||
if networked then
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- send updated data
|
||||
if not plc_state.no_modem then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
else
|
||||
if ticks_to_update == 0 then
|
||||
plc_comms.send_link_req()
|
||||
ticks_to_update = LINK_TICKS
|
||||
else
|
||||
ticks_to_update = ticks_to_update - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then
|
||||
-- got a packet
|
||||
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
if packet ~= nil then
|
||||
-- pass the packet onto the comms message queue
|
||||
smem.q.mq_comms_rx.push_packet(packet)
|
||||
end
|
||||
elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then
|
||||
-- haven't heard from server recently? shutdown reactor
|
||||
plc_comms.unlink()
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
|
||||
elseif event == "peripheral_detach" then
|
||||
-- peripheral disconnect
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "fissionReactor" then
|
||||
println_ts("reactor disconnected!")
|
||||
log.error("reactor disconnected!")
|
||||
plc_state.no_reactor = true
|
||||
plc_state.degraded = true
|
||||
elseif networked and type == "modem" then
|
||||
-- we only care if this is our wireless modem
|
||||
if device == plc_dev.modem then
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.error("comms modem disconnected!")
|
||||
plc_state.no_modem = true
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- try to scram reactor if it is still connected
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
end
|
||||
|
||||
plc_state.degraded = true
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
-- peripheral connect
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "fissionReactor" then
|
||||
-- reconnected reactor
|
||||
plc_dev.reactor = device
|
||||
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
println_ts("reactor reconnected.")
|
||||
log.info("reactor reconnected")
|
||||
plc_state.no_reactor = false
|
||||
|
||||
if plc_state.init_ok then
|
||||
rps.reconnect_reactor(plc_dev.reactor)
|
||||
if networked then
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
end
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if not networked or not plc_state.no_modem then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
elseif networked and type == "modem" then
|
||||
if device.isWireless() then
|
||||
-- reconnected modem
|
||||
plc_dev.modem = device
|
||||
|
||||
if plc_state.init_ok then
|
||||
plc_comms.reconnect_modem(plc_dev.modem)
|
||||
end
|
||||
|
||||
println_ts("wireless modem reconnected.")
|
||||
log.info("comms modem reconnected")
|
||||
plc_state.no_modem = false
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if not plc_state.no_reactor then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
else
|
||||
log.info("wired modem reconnected")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- if not init'd and no longer degraded, proceed to init
|
||||
if not plc_state.init_ok and not plc_state.degraded then
|
||||
plc_state.init_ok = true
|
||||
init()
|
||||
end
|
||||
elseif event == "clock_start" then
|
||||
-- start loop clock
|
||||
loop_clock.start()
|
||||
log.debug("main thread clock started")
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
log.info("terminate requested, main thread exiting")
|
||||
-- rps handles reactor shutdown
|
||||
plc_state.shutdown = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local plc_state = smem.plc_state
|
||||
|
||||
while not plc_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
-- if status is true, then we are probably exiting, so this won't matter
|
||||
-- if not, we need to restart the clock
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
|
||||
if not plc_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
os.queueEvent("clock_start")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- RPS operation thread
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__rps(smem)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("rps thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local networked = smem.networked
|
||||
local plc_state = smem.plc_state
|
||||
local plc_dev = smem.plc_dev
|
||||
|
||||
local rps_queue = smem.q.mq_rps
|
||||
|
||||
local was_linked = false
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
-- get reactor, may have changed do to disconnect/reconnect
|
||||
local reactor = plc_dev.reactor
|
||||
|
||||
-- RPS checks
|
||||
if plc_state.init_ok then
|
||||
-- SCRAM if no open connection
|
||||
if networked and not plc_comms.is_linked() then
|
||||
if was_linked then
|
||||
was_linked = false
|
||||
rps.trip_timeout()
|
||||
end
|
||||
else
|
||||
-- would do elseif not networked but there is no reason to do that extra operation
|
||||
was_linked = true
|
||||
end
|
||||
|
||||
-- if we tried to SCRAM but failed, keep trying
|
||||
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
if not plc_state.no_reactor and rps.is_tripped() and reactor.getStatus() then
|
||||
rps.scram()
|
||||
end
|
||||
|
||||
-- if we are in standalone mode, continuously reset RPS
|
||||
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
|
||||
if not networked then rps.reset() end
|
||||
|
||||
-- check safety (SCRAM occurs if tripped)
|
||||
if not plc_state.no_reactor then
|
||||
local rps_tripped, rps_status_string, rps_first = rps.check()
|
||||
|
||||
if rps_tripped and rps_first then
|
||||
println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string)
|
||||
if networked and not plc_state.no_modem then
|
||||
plc_comms.send_rps_alarm(rps_status_string)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check for messages in the message queue
|
||||
while rps_queue.ready() and not plc_state.shutdown do
|
||||
local msg = rps_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if plc_state.init_ok then
|
||||
if msg.message == MQ__RPS_CMD.SCRAM then
|
||||
-- SCRAM
|
||||
rps.scram()
|
||||
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
|
||||
-- lost peripheral(s)
|
||||
rps.trip_fault()
|
||||
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
|
||||
-- watchdog tripped
|
||||
rps.trip_timeout()
|
||||
println_ts("server timeout")
|
||||
log.warning("server timeout")
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
-- safe exit
|
||||
log.info("rps thread shutdown initiated")
|
||||
if plc_state.init_ok then
|
||||
if rps.scram() then
|
||||
println_ts("reactor disabled")
|
||||
log.info("rps thread reactor SCRAM OK")
|
||||
else
|
||||
println_ts("exiting, reactor failed to disable")
|
||||
log.error("rps thread failed to SCRAM reactor on exit")
|
||||
end
|
||||
end
|
||||
log.info("rps thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(RPS_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local plc_state = smem.plc_state
|
||||
|
||||
while not plc_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not plc_state.shutdown then
|
||||
if plc_state.init_ok then smem.plc_sys.rps.scram() end
|
||||
log.info("rps thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- communications sender thread
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__comms_tx(smem)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("comms tx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
local comms_queue = smem.q.mq_comms_tx
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
|
||||
-- check for messages in the message queue
|
||||
while comms_queue.ready() and not plc_state.shutdown do
|
||||
local msg = comms_queue.pop()
|
||||
|
||||
if msg ~= nil and plc_state.init_ok then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if msg.message == MQ__COMM_CMD.SEND_STATUS then
|
||||
-- send PLC/RPS status
|
||||
plc_comms.send_status(plc_state.degraded)
|
||||
plc_comms.send_rps_status()
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("comms tx thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local plc_state = smem.plc_state
|
||||
|
||||
while not plc_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms tx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- communications handler thread
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__comms_rx(smem)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("comms rx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
local setpoints = smem.setpoints
|
||||
|
||||
local comms_queue = smem.q.mq_comms_rx
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
|
||||
-- check for messages in the message queue
|
||||
while comms_queue.ready() and not plc_state.shutdown do
|
||||
local msg = comms_queue.pop()
|
||||
|
||||
if msg ~= nil and plc_state.init_ok then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
-- handle the packet (setpoints passed to update burn rate setpoint)
|
||||
-- (plc_state passed to check if degraded)
|
||||
plc_comms.handle_packet(msg.message, setpoints, plc_state)
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("comms rx thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local plc_state = smem.plc_state
|
||||
|
||||
while not plc_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms rx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- apply setpoints
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__setpoint_control(smem)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("setpoint control thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
local setpoints = smem.setpoints
|
||||
local plc_dev = smem.plc_dev
|
||||
|
||||
local last_update = util.time()
|
||||
local running = false
|
||||
|
||||
local last_sp_burn = 0.0
|
||||
|
||||
-- do not use the actual elapsed time, it could spike
|
||||
-- we do not want to have big jumps as that is what we are trying to avoid in the first place
|
||||
local min_elapsed_s = SP_CTRL_SLEEP / 1000.0
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local rps = smem.plc_sys.rps
|
||||
-- get reactor, may have changed do to disconnect/reconnect
|
||||
local reactor = plc_dev.reactor
|
||||
|
||||
if plc_state.init_ok and not plc_state.no_reactor then
|
||||
-- check if we should start ramping
|
||||
if setpoints.burn_rate_en and setpoints.burn_rate ~= last_sp_burn then
|
||||
if rps.is_active() then
|
||||
if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then
|
||||
-- update without ramp if <= 5 mB/t change
|
||||
log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t")
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
reactor.setBurnRate(setpoints.burn_rate)
|
||||
else
|
||||
log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t")
|
||||
running = true
|
||||
end
|
||||
|
||||
last_sp_burn = setpoints.burn_rate
|
||||
else
|
||||
last_sp_burn = 0.0
|
||||
end
|
||||
end
|
||||
|
||||
-- only check I/O if active to save on processing time
|
||||
if running then
|
||||
-- clear so we can later evaluate if we should keep running
|
||||
running = false
|
||||
|
||||
-- adjust burn rate (setpoints.burn_rate)
|
||||
if setpoints.burn_rate_en then
|
||||
if rps.is_active() then
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local current_burn_rate = reactor.getBurnRate()
|
||||
|
||||
-- we yielded, check enable again
|
||||
if setpoints.burn_rate_en and (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then
|
||||
-- calculate new burn rate
|
||||
local new_burn_rate = current_burn_rate
|
||||
|
||||
if setpoints.burn_rate > current_burn_rate then
|
||||
-- need to ramp up
|
||||
local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s)
|
||||
if new_burn_rate > setpoints.burn_rate then
|
||||
new_burn_rate = setpoints.burn_rate
|
||||
end
|
||||
else
|
||||
-- need to ramp down
|
||||
local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s)
|
||||
if new_burn_rate < setpoints.burn_rate then
|
||||
new_burn_rate = setpoints.burn_rate
|
||||
end
|
||||
end
|
||||
|
||||
-- set the burn rate
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
reactor.setBurnRate(new_burn_rate)
|
||||
|
||||
running = running or (new_burn_rate ~= setpoints.burn_rate)
|
||||
end
|
||||
else
|
||||
last_sp_burn = 0.0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("setpoint control thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(SP_CTRL_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local plc_state = smem.plc_state
|
||||
|
||||
while not plc_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("setpoint control thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return threads
|
||||
52
rtu/config.lua
Normal file
52
rtu/config.lua
Normal file
@@ -0,0 +1,52 @@
|
||||
local rsio = require("scada-common.rsio")
|
||||
|
||||
local config = {}
|
||||
|
||||
-- port to send packets TO server
|
||||
config.SERVER_PORT = 16000
|
||||
-- port to listen to incoming packets FROM server
|
||||
config.LISTEN_PORT = 15001
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
-- RTU peripheral devices (named: side/network device name)
|
||||
config.RTU_DEVICES = {
|
||||
{
|
||||
name = "boilerValve_0",
|
||||
index = 1,
|
||||
for_reactor = 1
|
||||
},
|
||||
{
|
||||
name = "turbineValve_0",
|
||||
index = 1,
|
||||
for_reactor = 1
|
||||
}
|
||||
}
|
||||
-- RTU redstone interface definitions
|
||||
config.RTU_REDSTONE = {
|
||||
{
|
||||
for_reactor = 1,
|
||||
io = {
|
||||
{
|
||||
channel = rsio.IO.WASTE_PO,
|
||||
side = "top",
|
||||
bundled_color = colors.blue
|
||||
},
|
||||
{
|
||||
channel = rsio.IO.WASTE_PU,
|
||||
side = "top",
|
||||
bundled_color = colors.cyan
|
||||
},
|
||||
{
|
||||
channel = rsio.IO.WASTE_AM,
|
||||
side = "top",
|
||||
bundled_color = colors.purple
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
48
rtu/dev/boiler_rtu.lua
Normal file
48
rtu/dev/boiler_rtu.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local boiler_rtu = {}
|
||||
|
||||
-- create new boiler (mek 10.0) device
|
||||
---@param boiler table
|
||||
function boiler_rtu.new(boiler)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- build properties
|
||||
unit.connect_input_reg(boiler.getBoilCapacity)
|
||||
unit.connect_input_reg(boiler.getSteamCapacity)
|
||||
unit.connect_input_reg(boiler.getWaterCapacity)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
|
||||
unit.connect_input_reg(boiler.getSuperheaters)
|
||||
unit.connect_input_reg(boiler.getMaxBoilRate)
|
||||
-- current state
|
||||
unit.connect_input_reg(boiler.getTemperature)
|
||||
unit.connect_input_reg(boiler.getBoilRate)
|
||||
-- tanks
|
||||
unit.connect_input_reg(boiler.getSteam)
|
||||
unit.connect_input_reg(boiler.getSteamNeeded)
|
||||
unit.connect_input_reg(boiler.getSteamFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getWater)
|
||||
unit.connect_input_reg(boiler.getWaterNeeded)
|
||||
unit.connect_input_reg(boiler.getWaterFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolant)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getCooledCoolant)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return boiler_rtu
|
||||
55
rtu/dev/boilerv_rtu.lua
Normal file
55
rtu/dev/boilerv_rtu.lua
Normal file
@@ -0,0 +1,55 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local boilerv_rtu = {}
|
||||
|
||||
-- create new boiler (mek 10.1+) device
|
||||
---@param boiler table
|
||||
function boilerv_rtu.new(boiler)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(boiler.isFormed)
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(boiler.getLength)
|
||||
unit.connect_input_reg(boiler.getWidth)
|
||||
unit.connect_input_reg(boiler.getHeight)
|
||||
unit.connect_input_reg(boiler.getMinPos)
|
||||
unit.connect_input_reg(boiler.getMaxPos)
|
||||
-- build properties
|
||||
unit.connect_input_reg(boiler.getBoilCapacity)
|
||||
unit.connect_input_reg(boiler.getSteamCapacity)
|
||||
unit.connect_input_reg(boiler.getWaterCapacity)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantCapacity)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantCapacity)
|
||||
unit.connect_input_reg(boiler.getSuperheaters)
|
||||
unit.connect_input_reg(boiler.getMaxBoilRate)
|
||||
unit.connect_input_reg(boiler.getEnvironmentalLoss)
|
||||
-- current state
|
||||
unit.connect_input_reg(boiler.getTemperature)
|
||||
unit.connect_input_reg(boiler.getBoilRate)
|
||||
-- tanks
|
||||
unit.connect_input_reg(boiler.getSteam)
|
||||
unit.connect_input_reg(boiler.getSteamNeeded)
|
||||
unit.connect_input_reg(boiler.getSteamFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getWater)
|
||||
unit.connect_input_reg(boiler.getWaterNeeded)
|
||||
unit.connect_input_reg(boiler.getWaterFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolant)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantNeeded)
|
||||
unit.connect_input_reg(boiler.getHeatedCoolantFilledPercentage)
|
||||
unit.connect_input_reg(boiler.getCooledCoolant)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantNeeded)
|
||||
unit.connect_input_reg(boiler.getCooledCoolantFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return boilerv_rtu
|
||||
30
rtu/dev/energymachine_rtu.lua
Normal file
30
rtu/dev/energymachine_rtu.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local energymachine_rtu = {}
|
||||
|
||||
-- create new energy machine device
|
||||
---@param machine table
|
||||
function energymachine_rtu.new(machine)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- build properties
|
||||
unit.connect_input_reg(machine.getTotalMaxEnergy)
|
||||
-- containers
|
||||
unit.connect_input_reg(machine.getTotalEnergy)
|
||||
unit.connect_input_reg(machine.getTotalEnergyNeeded)
|
||||
unit.connect_input_reg(machine.getTotalEnergyFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return energymachine_rtu
|
||||
26
rtu/dev/envd_rtu.lua
Normal file
26
rtu/dev/envd_rtu.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local envd_rtu = {}
|
||||
|
||||
-- create new environment detector device
|
||||
---@param envd table
|
||||
function envd_rtu.new(envd)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
unit.connect_input_reg(envd.getRadiation)
|
||||
unit.connect_input_reg(envd.getRadiationRaw)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return envd_rtu
|
||||
42
rtu/dev/imatrix_rtu.lua
Normal file
42
rtu/dev/imatrix_rtu.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local imatrix_rtu = {}
|
||||
|
||||
-- create new induction matrix (mek 10.1+) device
|
||||
---@param imatrix table
|
||||
function imatrix_rtu.new(imatrix)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(imatrix.isFormed)
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(imatrix.getLength)
|
||||
unit.connect_input_reg(imatrix.getWidth)
|
||||
unit.connect_input_reg(imatrix.getHeight)
|
||||
unit.connect_input_reg(imatrix.getMinPos)
|
||||
unit.connect_input_reg(imatrix.getMaxPos)
|
||||
-- build properties
|
||||
unit.connect_input_reg(imatrix.getMaxEnergy)
|
||||
unit.connect_input_reg(imatrix.getTransferCap)
|
||||
unit.connect_input_reg(imatrix.getInstalledCells)
|
||||
unit.connect_input_reg(imatrix.getInstalledProviders)
|
||||
-- I/O rates
|
||||
unit.connect_input_reg(imatrix.getLastInput)
|
||||
unit.connect_input_reg(imatrix.getLastOutput)
|
||||
-- tanks
|
||||
unit.connect_input_reg(imatrix.getEnergy)
|
||||
unit.connect_input_reg(imatrix.getEnergyNeeded)
|
||||
unit.connect_input_reg(imatrix.getEnergyFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return imatrix_rtu
|
||||
111
rtu/dev/redstone_rtu.lua
Normal file
111
rtu/dev/redstone_rtu.lua
Normal file
@@ -0,0 +1,111 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
local rsio = require("scada-common.rsio")
|
||||
|
||||
local redstone_rtu = {}
|
||||
|
||||
local digital_read = rsio.digital_read
|
||||
local digital_write = rsio.digital_write
|
||||
local digital_is_active = rsio.digital_is_active
|
||||
|
||||
-- create new redstone device
|
||||
function redstone_rtu.new()
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- get RTU interface
|
||||
local interface = unit.interface()
|
||||
|
||||
---@class rtu_rs_device
|
||||
--- extends rtu_device; fields added manually to please Lua diagnostics
|
||||
local public = {
|
||||
io_count = interface.io_count,
|
||||
read_coil = interface.read_coil,
|
||||
read_di = interface.read_di,
|
||||
read_holding_reg = interface.read_holding_reg,
|
||||
read_input_reg = interface.read_input_reg,
|
||||
write_coil = interface.write_coil,
|
||||
write_holding_reg = interface.write_holding_reg
|
||||
}
|
||||
|
||||
-- link digital input
|
||||
---@param side string
|
||||
---@param color integer
|
||||
function public.link_di(side, color)
|
||||
local f_read = nil
|
||||
|
||||
if color then
|
||||
f_read = function ()
|
||||
return digital_read(rs.testBundledInput(side, color))
|
||||
end
|
||||
else
|
||||
f_read = function ()
|
||||
return digital_read(rs.getInput(side))
|
||||
end
|
||||
end
|
||||
|
||||
unit.connect_di(f_read)
|
||||
end
|
||||
|
||||
-- link digital output
|
||||
---@param channel RS_IO
|
||||
---@param side string
|
||||
---@param color integer
|
||||
function public.link_do(channel, side, color)
|
||||
local f_read = nil
|
||||
local f_write = nil
|
||||
|
||||
if color then
|
||||
f_read = function ()
|
||||
return digital_read(colors.test(rs.getBundledOutput(side), color))
|
||||
end
|
||||
|
||||
f_write = function (level)
|
||||
local output = rs.getBundledOutput(side)
|
||||
|
||||
if digital_write(channel, level) then
|
||||
output = colors.combine(output, color)
|
||||
else
|
||||
output = colors.subtract(output, color)
|
||||
end
|
||||
|
||||
rs.setBundledOutput(side, output)
|
||||
end
|
||||
else
|
||||
f_read = function ()
|
||||
return digital_read(rs.getOutput(side))
|
||||
end
|
||||
|
||||
f_write = function (level)
|
||||
rs.setOutput(side, digital_is_active(channel, level))
|
||||
end
|
||||
end
|
||||
|
||||
unit.connect_coil(f_read, f_write)
|
||||
end
|
||||
|
||||
-- link analog input
|
||||
---@param side string
|
||||
function public.link_ai(side)
|
||||
unit.connect_input_reg(
|
||||
function ()
|
||||
return rs.getAnalogInput(side)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
-- link analog output
|
||||
---@param side string
|
||||
function public.link_ao(side)
|
||||
unit.connect_holding_reg(
|
||||
function ()
|
||||
return rs.getAnalogOutput(side)
|
||||
end,
|
||||
function (value)
|
||||
rs.setAnalogOutput(side, value)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return redstone_rtu
|
||||
37
rtu/dev/sna_rtu.lua
Normal file
37
rtu/dev/sna_rtu.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local sna_rtu = {}
|
||||
|
||||
-- create new solar neutron activator (sna) device
|
||||
---@param sna table
|
||||
function sna_rtu.new(sna)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- build properties
|
||||
unit.connect_input_reg(sna.getInputCapacity)
|
||||
unit.connect_input_reg(sna.getOutputCapacity)
|
||||
-- current state
|
||||
unit.connect_input_reg(sna.getProductionRate)
|
||||
unit.connect_input_reg(sna.getPeakProductionRate)
|
||||
-- tanks
|
||||
unit.connect_input_reg(sna.getInput)
|
||||
unit.connect_input_reg(sna.getInputNeeded)
|
||||
unit.connect_input_reg(sna.getInputFilledPercentage)
|
||||
unit.connect_input_reg(sna.getOutput)
|
||||
unit.connect_input_reg(sna.getOutputNeeded)
|
||||
unit.connect_input_reg(sna.getOutputFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return sna_rtu
|
||||
47
rtu/dev/sps_rtu.lua
Normal file
47
rtu/dev/sps_rtu.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local sps_rtu = {}
|
||||
|
||||
-- create new super-critical phase shifter (sps) device
|
||||
---@param sps table
|
||||
function sps_rtu.new(sps)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(sps.isFormed)
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(sps.getLength)
|
||||
unit.connect_input_reg(sps.getWidth)
|
||||
unit.connect_input_reg(sps.getHeight)
|
||||
unit.connect_input_reg(sps.getMinPos)
|
||||
unit.connect_input_reg(sps.getMaxPos)
|
||||
-- build properties
|
||||
unit.connect_input_reg(sps.getCoils)
|
||||
unit.connect_input_reg(sps.getInputCapacity)
|
||||
unit.connect_input_reg(sps.getOutputCapacity)
|
||||
unit.connect_input_reg(sps.getMaxEnergy)
|
||||
-- current state
|
||||
unit.connect_input_reg(sps.getProcessRate)
|
||||
-- tanks
|
||||
unit.connect_input_reg(sps.getInput)
|
||||
unit.connect_input_reg(sps.getInputNeeded)
|
||||
unit.connect_input_reg(sps.getInputFilledPercentage)
|
||||
unit.connect_input_reg(sps.getOutput)
|
||||
unit.connect_input_reg(sps.getOutputNeeded)
|
||||
unit.connect_input_reg(sps.getOutputFilledPercentage)
|
||||
unit.connect_input_reg(sps.getEnergy)
|
||||
unit.connect_input_reg(sps.getEnergyNeeded)
|
||||
unit.connect_input_reg(sps.getEnergyFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return sps_rtu
|
||||
43
rtu/dev/turbine_rtu.lua
Normal file
43
rtu/dev/turbine_rtu.lua
Normal file
@@ -0,0 +1,43 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local turbine_rtu = {}
|
||||
|
||||
-- create new turbine (mek 10.0) device
|
||||
---@param turbine table
|
||||
function turbine_rtu.new(turbine)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
-- none
|
||||
|
||||
-- coils --
|
||||
-- none
|
||||
|
||||
-- input registers --
|
||||
-- build properties
|
||||
unit.connect_input_reg(turbine.getBlades)
|
||||
unit.connect_input_reg(turbine.getCoils)
|
||||
unit.connect_input_reg(turbine.getVents)
|
||||
unit.connect_input_reg(turbine.getDispersers)
|
||||
unit.connect_input_reg(turbine.getCondensers)
|
||||
unit.connect_input_reg(turbine.getSteamCapacity)
|
||||
unit.connect_input_reg(turbine.getMaxFlowRate)
|
||||
unit.connect_input_reg(turbine.getMaxProduction)
|
||||
unit.connect_input_reg(turbine.getMaxWaterOutput)
|
||||
-- current state
|
||||
unit.connect_input_reg(turbine.getFlowRate)
|
||||
unit.connect_input_reg(turbine.getProductionRate)
|
||||
unit.connect_input_reg(turbine.getLastSteamInputRate)
|
||||
unit.connect_input_reg(turbine.getDumpingMode)
|
||||
-- tanks
|
||||
unit.connect_input_reg(turbine.getSteam)
|
||||
unit.connect_input_reg(turbine.getSteamNeeded)
|
||||
unit.connect_input_reg(turbine.getSteamFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
-- none
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return turbine_rtu
|
||||
54
rtu/dev/turbinev_rtu.lua
Normal file
54
rtu/dev/turbinev_rtu.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local turbinev_rtu = {}
|
||||
|
||||
-- create new turbine (mek 10.1+) device
|
||||
---@param turbine table
|
||||
function turbinev_rtu.new(turbine)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(turbine.isFormed)
|
||||
|
||||
-- coils --
|
||||
unit.connect_coil(function () turbine.incrementDumpingMode() end, function () end)
|
||||
unit.connect_coil(function () turbine.decrementDumpingMode() end, function () end)
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(turbine.getLength)
|
||||
unit.connect_input_reg(turbine.getWidth)
|
||||
unit.connect_input_reg(turbine.getHeight)
|
||||
unit.connect_input_reg(turbine.getMinPos)
|
||||
unit.connect_input_reg(turbine.getMaxPos)
|
||||
-- build properties
|
||||
unit.connect_input_reg(turbine.getBlades)
|
||||
unit.connect_input_reg(turbine.getCoils)
|
||||
unit.connect_input_reg(turbine.getVents)
|
||||
unit.connect_input_reg(turbine.getDispersers)
|
||||
unit.connect_input_reg(turbine.getCondensers)
|
||||
unit.connect_input_reg(turbine.getSteamCapacity)
|
||||
unit.connect_input_reg(turbine.getMaxEnergy)
|
||||
unit.connect_input_reg(turbine.getMaxFlowRate)
|
||||
unit.connect_input_reg(turbine.getMaxProduction)
|
||||
unit.connect_input_reg(turbine.getMaxWaterOutput)
|
||||
-- current state
|
||||
unit.connect_input_reg(turbine.getFlowRate)
|
||||
unit.connect_input_reg(turbine.getProductionRate)
|
||||
unit.connect_input_reg(turbine.getLastSteamInputRate)
|
||||
unit.connect_input_reg(turbine.getDumpingMode)
|
||||
-- tanks/containers
|
||||
unit.connect_input_reg(turbine.getSteam)
|
||||
unit.connect_input_reg(turbine.getSteamNeeded)
|
||||
unit.connect_input_reg(turbine.getSteamFilledPercentage)
|
||||
unit.connect_input_reg(turbine.getEnergy)
|
||||
unit.connect_input_reg(turbine.getEnergyNeeded)
|
||||
unit.connect_input_reg(turbine.getEnergyFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
unit.connect_holding_reg(turbine.setDumpingMode, turbine.getDumpingMode)
|
||||
|
||||
return unit.interface()
|
||||
end
|
||||
|
||||
return turbinev_rtu
|
||||
438
rtu/modbus.lua
Normal file
438
rtu/modbus.lua
Normal file
@@ -0,0 +1,438 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local modbus = {}
|
||||
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
local MODBUS_EXCODE = types.MODBUS_EXCODE
|
||||
|
||||
-- new modbus comms handler object
|
||||
---@param rtu_dev rtu_device|rtu_rs_device RTU device
|
||||
---@param use_parallel_read boolean whether or not to use parallel calls when reading
|
||||
function modbus.new(rtu_dev, use_parallel_read)
|
||||
local self = {
|
||||
rtu = rtu_dev,
|
||||
use_parallel = use_parallel_read
|
||||
}
|
||||
|
||||
---@class modbus
|
||||
local public = {}
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
---@param c_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table readings
|
||||
local function _1_read_coils(c_addr_start, count)
|
||||
local tasks = {}
|
||||
local readings = {}
|
||||
local access_fault = false
|
||||
local _, coils, _, _ = self.rtu.io_count()
|
||||
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = c_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_coil(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_coil(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
end
|
||||
end
|
||||
else
|
||||
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
---@param di_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table readings
|
||||
local function _2_read_discrete_inputs(di_addr_start, count)
|
||||
local tasks = {}
|
||||
local readings = {}
|
||||
local access_fault = false
|
||||
local discrete_inputs, _, _, _ = self.rtu.io_count()
|
||||
local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = di_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_di(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_di(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
end
|
||||
end
|
||||
else
|
||||
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
---@param hr_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table readings
|
||||
local function _3_read_multiple_holding_registers(hr_addr_start, count)
|
||||
local tasks = {}
|
||||
local readings = {}
|
||||
local access_fault = false
|
||||
local _, _, _, hold_regs = self.rtu.io_count()
|
||||
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = hr_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_holding_reg(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_holding_reg(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
end
|
||||
end
|
||||
else
|
||||
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
---@param ir_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table readings
|
||||
local function _4_read_input_registers(ir_addr_start, count)
|
||||
local tasks = {}
|
||||
local readings = {}
|
||||
local access_fault = false
|
||||
local _, _, input_regs, _ = self.rtu.io_count()
|
||||
local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = ir_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_input_reg(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_input_reg(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
end
|
||||
end
|
||||
else
|
||||
readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
---@param c_addr integer
|
||||
---@param value any
|
||||
---@return boolean ok, MODBUS_EXCODE|nil
|
||||
local function _5_write_single_coil(c_addr, value)
|
||||
local response = nil
|
||||
local _, coils, _, _ = self.rtu.io_count()
|
||||
local return_ok = c_addr <= coils
|
||||
|
||||
if return_ok then
|
||||
local access_fault = self.rtu.write_coil(c_addr, value)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
end
|
||||
else
|
||||
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, response
|
||||
end
|
||||
|
||||
---@param hr_addr integer
|
||||
---@param value any
|
||||
---@return boolean ok, MODBUS_EXCODE|nil
|
||||
local function _6_write_single_holding_register(hr_addr, value)
|
||||
local response = nil
|
||||
local _, _, _, hold_regs = self.rtu.io_count()
|
||||
local return_ok = hr_addr <= hold_regs
|
||||
|
||||
if return_ok then
|
||||
local access_fault = self.rtu.write_holding_reg(hr_addr, value)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
end
|
||||
else
|
||||
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, response
|
||||
end
|
||||
|
||||
---@param c_addr_start integer
|
||||
---@param values any
|
||||
---@return boolean ok, MODBUS_EXCODE|nil
|
||||
local function _15_write_multiple_coils(c_addr_start, values)
|
||||
local response = nil
|
||||
local _, coils, _, _ = self.rtu.io_count()
|
||||
local count = #values
|
||||
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = c_addr_start + i - 1
|
||||
local access_fault = self.rtu.write_coil(addr, values[i])
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, response
|
||||
end
|
||||
|
||||
---@param hr_addr_start integer
|
||||
---@param values any
|
||||
---@return boolean ok, MODBUS_EXCODE|nil
|
||||
local function _16_write_multiple_holding_registers(hr_addr_start, values)
|
||||
local response = nil
|
||||
local _, _, _, hold_regs = self.rtu.io_count()
|
||||
local count = #values
|
||||
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = hr_addr_start + i - 1
|
||||
local access_fault = self.rtu.write_holding_reg(addr, values[i])
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
response = MODBUS_EXCODE.SERVER_DEVICE_FAIL
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR
|
||||
end
|
||||
|
||||
return return_ok, response
|
||||
end
|
||||
|
||||
-- validate a request without actually executing it
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.check_request(packet)
|
||||
local return_code = true
|
||||
local response = { MODBUS_EXCODE.ACKNOWLEDGE }
|
||||
|
||||
if packet.length == 2 then
|
||||
-- handle by function code
|
||||
if packet.func_code == MODBUS_FCODE.READ_COILS then
|
||||
elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then
|
||||
elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then
|
||||
elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGS then
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
|
||||
else
|
||||
-- unknown function
|
||||
return_code = false
|
||||
response = { MODBUS_EXCODE.ILLEGAL_FUNCTION }
|
||||
end
|
||||
else
|
||||
-- invalid length
|
||||
return_code = false
|
||||
response = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
|
||||
end
|
||||
|
||||
-- default is to echo back
|
||||
local func_code = packet.func_code
|
||||
|
||||
-- echo back with error flag, on success the "error" will be acknowledgement
|
||||
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
|
||||
-- create reply
|
||||
local reply = comms.modbus_packet()
|
||||
reply.make(packet.txn_id, packet.unit_id, func_code, response)
|
||||
|
||||
return return_code, reply
|
||||
end
|
||||
|
||||
-- handle a MODBUS TCP packet and generate a reply
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.handle_packet(packet)
|
||||
local return_code = true
|
||||
local response = nil
|
||||
|
||||
if packet.length == 2 then
|
||||
-- handle by function code
|
||||
if packet.func_code == MODBUS_FCODE.READ_COILS then
|
||||
return_code, response = _1_read_coils(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then
|
||||
return_code, response = _2_read_discrete_inputs(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then
|
||||
return_code, response = _3_read_multiple_holding_registers(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGS then
|
||||
return_code, response = _4_read_input_registers(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then
|
||||
return_code, response = _5_write_single_coil(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then
|
||||
return_code, response = _6_write_single_holding_register(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then
|
||||
return_code, response = _15_write_multiple_coils(packet.data[1], packet.data[2])
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
|
||||
return_code, response = _16_write_multiple_holding_registers(packet.data[1], packet.data[2])
|
||||
else
|
||||
-- unknown function
|
||||
return_code = false
|
||||
response = MODBUS_EXCODE.ILLEGAL_FUNCTION
|
||||
end
|
||||
else
|
||||
-- invalid length
|
||||
return_code = false
|
||||
end
|
||||
|
||||
-- default is to echo back
|
||||
local func_code = packet.func_code
|
||||
if not return_code then
|
||||
-- echo back with error flag
|
||||
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
end
|
||||
|
||||
if type(response) == "table" then
|
||||
elseif type(response) == "nil" then
|
||||
response = {}
|
||||
else
|
||||
response = { response }
|
||||
end
|
||||
|
||||
-- create reply
|
||||
local reply = comms.modbus_packet()
|
||||
reply.make(packet.txn_id, packet.unit_id, func_code, response)
|
||||
|
||||
return return_code, reply
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- return a SERVER_DEVICE_BUSY error reply
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__srv_device_busy(packet)
|
||||
-- reply back with error flag and exception code
|
||||
local reply = comms.modbus_packet()
|
||||
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
|
||||
reply.make(packet.txn_id, packet.unit_id, fcode, data)
|
||||
return reply
|
||||
end
|
||||
|
||||
-- return a NEG_ACKNOWLEDGE error reply
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__neg_ack(packet)
|
||||
-- reply back with error flag and exception code
|
||||
local reply = comms.modbus_packet()
|
||||
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
|
||||
reply.make(packet.txn_id, packet.unit_id, fcode, data)
|
||||
return reply
|
||||
end
|
||||
|
||||
-- return a GATEWAY_PATH_UNAVAILABLE error reply
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__gw_unavailable(packet)
|
||||
-- reply back with error flag and exception code
|
||||
local reply = comms.modbus_packet()
|
||||
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
|
||||
reply.make(packet.txn_id, packet.unit_id, fcode, data)
|
||||
return reply
|
||||
end
|
||||
|
||||
return modbus
|
||||
419
rtu/rtu.lua
Normal file
419
rtu/rtu.lua
Normal file
@@ -0,0 +1,419 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local modbus = require("rtu.modbus")
|
||||
|
||||
local rtu = {}
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
-- create a new RTU
|
||||
function rtu.init_unit()
|
||||
local self = {
|
||||
discrete_inputs = {},
|
||||
coils = {},
|
||||
input_regs = {},
|
||||
holding_regs = {},
|
||||
io_count_cache = { 0, 0, 0, 0 }
|
||||
}
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
---@class rtu_device
|
||||
local public = {}
|
||||
|
||||
---@class rtu
|
||||
local protected = {}
|
||||
|
||||
-- refresh IO count
|
||||
local function _count_io()
|
||||
self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs }
|
||||
end
|
||||
|
||||
-- return IO count
|
||||
---@return integer discrete_inputs, integer coils, integer input_regs, integer holding_regs
|
||||
function public.io_count()
|
||||
return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4]
|
||||
end
|
||||
|
||||
-- discrete inputs: single bit read-only
|
||||
|
||||
-- connect discrete input
|
||||
---@param f function
|
||||
---@return integer count count of discrete inputs
|
||||
function protected.connect_di(f)
|
||||
insert(self.discrete_inputs, { read = f })
|
||||
_count_io()
|
||||
return #self.discrete_inputs
|
||||
end
|
||||
|
||||
-- read discrete input
|
||||
---@param di_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_di(di_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.discrete_inputs[di_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
end
|
||||
|
||||
-- coils: single bit read-write
|
||||
|
||||
-- connect coil
|
||||
---@param f_read function
|
||||
---@param f_write function
|
||||
---@return integer count count of coils
|
||||
function protected.connect_coil(f_read, f_write)
|
||||
insert(self.coils, { read = f_read, write = f_write })
|
||||
_count_io()
|
||||
return #self.coils
|
||||
end
|
||||
|
||||
-- read coil
|
||||
---@param coil_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_coil(coil_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.coils[coil_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
end
|
||||
|
||||
-- write coil
|
||||
---@param coil_addr integer
|
||||
---@param value any
|
||||
---@return boolean access_fault
|
||||
function public.write_coil(coil_addr, value)
|
||||
ppm.clear_fault()
|
||||
self.coils[coil_addr].write(value)
|
||||
return ppm.is_faulted()
|
||||
end
|
||||
|
||||
-- input registers: multi-bit read-only
|
||||
|
||||
-- connect input register
|
||||
---@param f function
|
||||
---@return integer count count of input registers
|
||||
function protected.connect_input_reg(f)
|
||||
insert(self.input_regs, { read = f })
|
||||
_count_io()
|
||||
return #self.input_regs
|
||||
end
|
||||
|
||||
-- read input register
|
||||
---@param reg_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_input_reg(reg_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.input_regs[reg_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
end
|
||||
|
||||
-- holding registers: multi-bit read-write
|
||||
|
||||
-- connect holding register
|
||||
---@param f_read function
|
||||
---@param f_write function
|
||||
---@return integer count count of holding registers
|
||||
function protected.connect_holding_reg(f_read, f_write)
|
||||
insert(self.holding_regs, { read = f_read, write = f_write })
|
||||
_count_io()
|
||||
return #self.holding_regs
|
||||
end
|
||||
|
||||
-- read holding register
|
||||
---@param reg_addr integer
|
||||
---@return any value, boolean access_fault
|
||||
function public.read_holding_reg(reg_addr)
|
||||
ppm.clear_fault()
|
||||
local value = self.holding_regs[reg_addr].read()
|
||||
return value, ppm.is_faulted()
|
||||
end
|
||||
|
||||
-- write holding register
|
||||
---@param reg_addr integer
|
||||
---@param value any
|
||||
---@return boolean access_fault
|
||||
function public.write_holding_reg(reg_addr, value)
|
||||
ppm.clear_fault()
|
||||
self.holding_regs[reg_addr].write(value)
|
||||
return ppm.is_faulted()
|
||||
end
|
||||
|
||||
-- public RTU device access
|
||||
|
||||
-- get the public interface to this RTU
|
||||
function protected.interface()
|
||||
return public
|
||||
end
|
||||
|
||||
return protected
|
||||
end
|
||||
|
||||
-- RTU Communications
|
||||
---@param version string
|
||||
---@param modem table
|
||||
---@param local_port integer
|
||||
---@param server_port integer
|
||||
---@param conn_watchdog watchdog
|
||||
function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
|
||||
local self = {
|
||||
version = version,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
txn_id = 0,
|
||||
modem = modem,
|
||||
s_port = server_port,
|
||||
l_port = local_port,
|
||||
conn_watchdog = conn_watchdog
|
||||
}
|
||||
|
||||
---@class rtu_comms
|
||||
local public = {}
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
-- open modem
|
||||
if not self.modem.isOpen(self.l_port) then
|
||||
self.modem.open(self.l_port)
|
||||
end
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- send a scada management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- send a MODBUS TCP packet
|
||||
---@param m_pkt modbus_packet
|
||||
function public.send_modbus(m_pkt)
|
||||
local s_pkt = comms.scada_packet()
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- reconnect a newly connected modem
|
||||
---@param modem table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_modem(modem)
|
||||
self.modem = modem
|
||||
|
||||
-- open modem
|
||||
if not self.modem.isOpen(self.l_port) then
|
||||
self.modem.open(self.l_port)
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink from the server
|
||||
---@param rtu_state rtu_state
|
||||
function public.unlink(rtu_state)
|
||||
rtu_state.linked = false
|
||||
self.r_seq_num = nil
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
---@param rtu_state rtu_state
|
||||
function public.close(rtu_state)
|
||||
self.conn_watchdog.cancel()
|
||||
public.unlink(rtu_state)
|
||||
_send(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
end
|
||||
|
||||
-- send capability advertisement
|
||||
---@param units table
|
||||
function public.send_advertisement(units)
|
||||
local advertisement = { self.version }
|
||||
|
||||
for i = 1, #units do
|
||||
local unit = units[i] --@type rtu_unit_registry_entry
|
||||
local type = comms.rtu_t_to_unit_type(unit.type)
|
||||
|
||||
if type ~= nil then
|
||||
local advert = {
|
||||
type,
|
||||
unit.index,
|
||||
unit.reactor
|
||||
}
|
||||
|
||||
if type == RTU_UNIT_TYPES.REDSTONE then
|
||||
insert(advert, unit.device)
|
||||
end
|
||||
|
||||
insert(advertisement, advert)
|
||||
end
|
||||
end
|
||||
|
||||
_send(SCADA_MGMT_TYPES.RTU_ADVERT, advertisement)
|
||||
end
|
||||
|
||||
-- parse a MODBUS/SCADA packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return modbus_frame|mgmt_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local s_pkt = comms.scada_packet()
|
||||
|
||||
-- parse packet as generic SCADA packet
|
||||
s_pkt.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as MODBUS TCP packet
|
||||
if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
local m_pkt = comms.modbus_packet()
|
||||
if m_pkt.decode(s_pkt) then
|
||||
pkt = m_pkt.get()
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
else
|
||||
log.error("illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle a MODBUS/SCADA packet
|
||||
---@param packet modbus_frame|mgmt_frame
|
||||
---@param units table
|
||||
---@param rtu_state rtu_state
|
||||
function public.handle_packet(packet, units, rtu_state)
|
||||
if packet ~= nil then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif rtu_state.linked and self.r_seq_num >= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if protocol == PROTOCOLS.MODBUS_TCP then
|
||||
local return_code = false
|
||||
local reply = modbus.reply__neg_ack(packet)
|
||||
|
||||
-- handle MODBUS instruction
|
||||
if packet.unit_id <= #units then
|
||||
local unit = units[packet.unit_id] ---@type rtu_unit_registry_entry
|
||||
local unit_dbg_tag = " (unit " .. packet.unit_id .. ")"
|
||||
|
||||
if unit.name == "redstone_io" then
|
||||
-- immediately execute redstone RTU requests
|
||||
return_code, reply = unit.modbus_io.handle_packet(packet)
|
||||
if not return_code then
|
||||
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
|
||||
end
|
||||
else
|
||||
-- check validity then pass off to unit comms thread
|
||||
return_code, reply = unit.modbus_io.check_request(packet)
|
||||
if return_code then
|
||||
-- check if there are more than 3 active transactions
|
||||
-- still queue the packet, but this may indicate a problem
|
||||
if unit.pkt_queue.length() > 3 then
|
||||
reply = modbus.reply__srv_device_busy(packet)
|
||||
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
|
||||
" transactions already in the queue" .. unit_dbg_tag)
|
||||
end
|
||||
|
||||
-- always queue the command even if busy
|
||||
unit.pkt_queue.push_packet(packet)
|
||||
else
|
||||
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- unit ID out of range?
|
||||
reply = modbus.reply__gw_unavailable(packet)
|
||||
log.error("received MODBUS packet for non-existent unit")
|
||||
end
|
||||
|
||||
public.send_modbus(reply)
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
-- SCADA management packet
|
||||
if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 500 then
|
||||
log.warning("RTU KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("RTU RTT = " .. trip_time .. "ms")
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
-- close connection
|
||||
self.conn_watchdog.cancel()
|
||||
public.unlink(rtu_state)
|
||||
println_ts("server connection closed by remote host")
|
||||
log.warning("server connection closed by remote host")
|
||||
elseif packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then
|
||||
-- acknowledgement
|
||||
rtu_state.linked = true
|
||||
self.r_seq_num = nil
|
||||
elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then
|
||||
-- request for capabilities again
|
||||
public.send_advertisement(units)
|
||||
else
|
||||
-- not supported
|
||||
log.warning("RTU got unexpected SCADA message type " .. packet.type)
|
||||
end
|
||||
else
|
||||
-- should be unreachable assuming packet is from parse_packet()
|
||||
log.error("illegal packet type " .. protocol, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return rtu
|
||||
367
rtu/startup.lua
Normal file
367
rtu/startup.lua
Normal file
@@ -0,0 +1,367 @@
|
||||
--
|
||||
-- RTU: Remote Terminal Unit
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("rtu.config")
|
||||
local modbus = require("rtu.modbus")
|
||||
local rtu = require("rtu.rtu")
|
||||
local threads = require("rtu.threads")
|
||||
|
||||
local redstone_rtu = require("rtu.dev.redstone_rtu")
|
||||
local boiler_rtu = require("rtu.dev.boiler_rtu")
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local energymachine_rtu = require("rtu.dev.energymachine_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local turbine_rtu = require("rtu.dev.turbine_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "beta-v0.7.12"
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_port(config.SERVER_PORT)
|
||||
cfv.assert_port(config.LISTEN_PORT)
|
||||
cfv.assert_type_str(config.LOG_PATH)
|
||||
cfv.assert_type_int(config.LOG_MODE)
|
||||
cfv.assert_type_table(config.RTU_DEVICES)
|
||||
cfv.assert_type_table(config.RTU_REDSTONE)
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING rtu.startup " .. RTU_VERSION)
|
||||
log.info("========================================")
|
||||
println(">> RTU GATEWAY " .. RTU_VERSION .. " <<")
|
||||
|
||||
----------------------------------------
|
||||
-- startup
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
---@class rtu_shared_memory
|
||||
local __shared_memory = {
|
||||
-- RTU system state flags
|
||||
---@class rtu_state
|
||||
rtu_state = {
|
||||
linked = false,
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- core RTU devices
|
||||
rtu_dev = {
|
||||
modem = ppm.get_wireless_modem()
|
||||
},
|
||||
|
||||
-- system objects
|
||||
rtu_sys = {
|
||||
rtu_comms = nil, ---@type rtu_comms
|
||||
conn_watchdog = nil, ---@type watchdog
|
||||
units = {} ---@type table
|
||||
},
|
||||
|
||||
-- message queues
|
||||
q = {
|
||||
mq_comms = mqueue.new()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.rtu_dev
|
||||
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
|
||||
|
||||
----------------------------------------
|
||||
-- interpret config and init units
|
||||
----------------------------------------
|
||||
|
||||
local units = __shared_memory.rtu_sys.units
|
||||
|
||||
local rtu_redstone = config.RTU_REDSTONE
|
||||
local rtu_devices = config.RTU_DEVICES
|
||||
|
||||
-- configure RTU gateway based on config file definitions
|
||||
local function configure()
|
||||
-- redstone interfaces
|
||||
for entry_idx = 1, #rtu_redstone do
|
||||
local rs_rtu = redstone_rtu.new()
|
||||
local io_table = rtu_redstone[entry_idx].io
|
||||
local io_reactor = rtu_redstone[entry_idx].for_reactor
|
||||
|
||||
-- CHECK: reactor ID must be >= to 1
|
||||
if (not util.is_int(io_reactor)) or (io_reactor <= 0) then
|
||||
println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 1"))
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: io table exists
|
||||
if type(io_table) ~= "table" then
|
||||
println(util.c("configure> redstone entry #", entry_idx, " no IO table found"))
|
||||
return false
|
||||
end
|
||||
|
||||
local capabilities = {}
|
||||
|
||||
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
|
||||
|
||||
local continue = true
|
||||
|
||||
-- check for duplicate entries
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
|
||||
-- duplicate entry
|
||||
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
|
||||
" with already defined redstone I/O")
|
||||
println(message)
|
||||
log.warning(message)
|
||||
continue = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- not a duplicate
|
||||
if continue then
|
||||
for i = 1, #io_table do
|
||||
local valid = false
|
||||
local conf = io_table[i]
|
||||
|
||||
-- verify configuration
|
||||
if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then
|
||||
if conf.bundled_color then
|
||||
valid = rsio.is_color(conf.bundled_color)
|
||||
else
|
||||
valid = true
|
||||
end
|
||||
end
|
||||
|
||||
if not valid then
|
||||
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
|
||||
" (for reactor ", io_reactor, ")")
|
||||
println(message)
|
||||
log.error(message)
|
||||
return false
|
||||
else
|
||||
-- link redstone in RTU
|
||||
local mode = rsio.get_io_mode(conf.channel)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(capabilities, conf.channel) then
|
||||
local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
rs_rtu.link_di(conf.side, conf.bundled_color)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
||||
rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(capabilities, conf.channel) then
|
||||
local message = util.c("configure> skipping duplicate input for channel ", rsio.to_string(conf.channel), " on side ", conf.side)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
rs_rtu.link_ai(conf.side)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
||||
rs_rtu.link_ao(conf.side)
|
||||
else
|
||||
-- should be unreachable code, we already validated channels
|
||||
log.error("configure> fell through if chain attempting to identify IO mode", true)
|
||||
println("configure> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
table.insert(capabilities, conf.channel)
|
||||
|
||||
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.channel),
|
||||
" (", conf.side, ") for reactor ", io_reactor))
|
||||
end
|
||||
end
|
||||
|
||||
---@class rtu_unit_registry_entry
|
||||
local unit = {
|
||||
name = "redstone_io",
|
||||
type = rtu_t.redstone,
|
||||
index = entry_idx,
|
||||
reactor = io_reactor,
|
||||
device = capabilities, -- use device field for redstone channels
|
||||
rtu = rs_rtu,
|
||||
modbus_io = modbus.new(rs_rtu, false),
|
||||
pkt_queue = nil,
|
||||
thread = nil
|
||||
}
|
||||
|
||||
table.insert(units, unit)
|
||||
|
||||
log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor))
|
||||
end
|
||||
end
|
||||
|
||||
-- mounted peripherals
|
||||
for i = 1, #rtu_devices do
|
||||
local name = rtu_devices[i].name
|
||||
local index = rtu_devices[i].index
|
||||
local for_reactor = rtu_devices[i].for_reactor
|
||||
|
||||
-- CHECK: name is a string
|
||||
if type(name) ~= "string" then
|
||||
println(util.c("configure> device entry #", i, ": device ", name, " isn't a string"))
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index is an integer >= 1
|
||||
if (not util.is_int(index)) or (index <= 0) then
|
||||
println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1"))
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: reactor is an integer >= 1
|
||||
if (not util.is_int(for_reactor)) or (for_reactor <= 0) then
|
||||
println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 1"))
|
||||
return false
|
||||
end
|
||||
|
||||
local device = ppm.get_periph(name)
|
||||
|
||||
if device == nil then
|
||||
local message = util.c("configure> '", name, "' not found")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else
|
||||
local type = ppm.get_type(name)
|
||||
local rtu_iface = nil ---@type rtu_device
|
||||
local rtu_type = ""
|
||||
|
||||
if type == "boiler" then
|
||||
-- boiler multiblock
|
||||
rtu_type = rtu_t.boiler
|
||||
rtu_iface = boiler_rtu.new(device)
|
||||
elseif type == "boilerValve" then
|
||||
-- boiler multiblock (10.1+)
|
||||
rtu_type = rtu_t.boiler_valve
|
||||
rtu_iface = boilerv_rtu.new(device)
|
||||
elseif type == "turbine" then
|
||||
-- turbine multiblock
|
||||
rtu_type = rtu_t.turbine
|
||||
rtu_iface = turbine_rtu.new(device)
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock (10.1+)
|
||||
rtu_type = rtu_t.turbine_valve
|
||||
rtu_iface = turbinev_rtu.new(device)
|
||||
elseif type == "mekanismMachine" then
|
||||
-- assumed to be an induction matrix multiblock, pre Mekanism 10.1
|
||||
-- also works with energy cubes
|
||||
rtu_type = rtu_t.energy_machine
|
||||
rtu_iface = energymachine_rtu.new(device)
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock (10.1+)
|
||||
rtu_type = rtu_t.induction_matrix
|
||||
rtu_iface = imatrix_rtu.new(device)
|
||||
elseif type == "environmentDetector" then
|
||||
-- advanced peripherals environment detector
|
||||
rtu_type = rtu_t.env_detector
|
||||
rtu_iface = envd_rtu.new(device)
|
||||
else
|
||||
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
|
||||
println_ts(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
if rtu_iface ~= nil then
|
||||
---@class rtu_unit_registry_entry
|
||||
local rtu_unit = {
|
||||
name = name,
|
||||
type = rtu_type,
|
||||
index = index,
|
||||
reactor = for_reactor,
|
||||
device = device,
|
||||
rtu = rtu_iface,
|
||||
modbus_io = modbus.new(rtu_iface, true),
|
||||
pkt_queue = mqueue.new(),
|
||||
thread = nil
|
||||
}
|
||||
|
||||
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
|
||||
|
||||
table.insert(units, rtu_unit)
|
||||
|
||||
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- we made it through all that trusting-user-to-write-a-config-file chaos
|
||||
return true
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
log.debug("boot> running configure()")
|
||||
|
||||
if configure() then
|
||||
-- start connection watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(5)
|
||||
log.debug("boot> conn watchdog started")
|
||||
|
||||
-- setup comms
|
||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog)
|
||||
log.debug("boot> comms init")
|
||||
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local comms_thread = threads.thread__comms(__shared_memory)
|
||||
|
||||
-- assemble thread list
|
||||
local _threads = { main_thread.p_exec, comms_thread.p_exec }
|
||||
for i = 1, #units do
|
||||
if units[i].thread ~= nil then
|
||||
table.insert(_threads, units[i].thread.p_exec)
|
||||
end
|
||||
end
|
||||
|
||||
-- run threads
|
||||
parallel.waitForAll(table.unpack(_threads))
|
||||
else
|
||||
println("configuration failed, exiting...")
|
||||
end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
321
rtu/threads.lua
Normal file
321
rtu/threads.lua
Normal file
@@ -0,0 +1,321 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local boiler_rtu = require("rtu.dev.boiler_rtu")
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local energymachine_rtu = require("rtu.dev.energymachine_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local turbine_rtu = require("rtu.dev.turbine_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local modbus = require("rtu.modbus")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
|
||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
-- main thread
|
||||
---@param smem rtu_shared_memory
|
||||
function threads.thread__main(smem)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("main thread start")
|
||||
|
||||
-- main loop clock
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
local rtu_dev = smem.rtu_dev
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local conn_watchdog = smem.rtu_sys.conn_watchdog
|
||||
local units = smem.rtu_sys.units
|
||||
|
||||
-- start unlinked (in case of restart)
|
||||
rtu_comms.unlink(rtu_state)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
if event == "timer" and loop_clock.is_clock(param1) then
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- period tick, if we are not linked send advertisement
|
||||
if not rtu_state.linked then
|
||||
-- advertise units
|
||||
rtu_comms.send_advertisement(units)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
if packet ~= nil then
|
||||
-- pass the packet onto the comms message queue
|
||||
smem.q.mq_comms.push_packet(packet)
|
||||
end
|
||||
elseif event == "timer" and conn_watchdog.is_timer(param1) then
|
||||
-- haven't heard from server recently? unlink
|
||||
rtu_comms.unlink(rtu_state)
|
||||
elseif event == "peripheral_detach" then
|
||||
-- handle loss of a device
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only care if this is our wireless modem
|
||||
if device == rtu_dev.modem then
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.warning("comms modem disconnected!")
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
else
|
||||
for i = 1, #units do
|
||||
-- find disconnected device
|
||||
if units[i].device == device then
|
||||
-- we are going to let the PPM prevent crashes
|
||||
-- return fault flags/codes to MODBUS queries
|
||||
local unit = units[i]
|
||||
println_ts("lost the " .. unit.type .. " on interface " .. unit.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
-- peripheral connect
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() then
|
||||
-- reconnected modem
|
||||
rtu_dev.modem = device
|
||||
rtu_comms.reconnect_modem(rtu_dev.modem)
|
||||
|
||||
println_ts("wireless modem reconnected.")
|
||||
log.info("comms modem reconnected.")
|
||||
else
|
||||
log.info("wired modem reconnected.")
|
||||
end
|
||||
else
|
||||
-- relink lost peripheral to correct unit entry
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
|
||||
-- find disconnected device to reconnect
|
||||
if unit.name == param1 then
|
||||
-- found, re-link
|
||||
unit.device = device
|
||||
|
||||
if unit.type == rtu_t.boiler then
|
||||
unit.rtu = boiler_rtu.new(device)
|
||||
elseif unit.type == rtu_t.boiler_valve then
|
||||
unit.rtu = boilerv_rtu.new(device)
|
||||
elseif unit.type == rtu_t.turbine then
|
||||
unit.rtu = turbine_rtu.new(device)
|
||||
elseif unit.type == rtu_t.turbine_valve then
|
||||
unit.rtu = turbinev_rtu.new(device)
|
||||
elseif unit.type == rtu_t.energy_machine then
|
||||
unit.rtu = energymachine_rtu.new(device)
|
||||
elseif unit.type == rtu_t.induction_matrix then
|
||||
unit.rtu = imatrix_rtu.new(device)
|
||||
end
|
||||
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
|
||||
println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
rtu_state.shutdown = true
|
||||
log.info("terminate requested, main thread exiting")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local rtu_state = smem.rtu_state
|
||||
|
||||
while not rtu_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info("main thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- communications handler thread
|
||||
---@param smem rtu_shared_memory
|
||||
function threads.thread__comms(smem)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("comms thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local units = smem.rtu_sys.units
|
||||
|
||||
local comms_queue = smem.q.mq_comms
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- check for messages in the message queue
|
||||
while comms_queue.ready() and not rtu_state.shutdown do
|
||||
local msg = comms_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
-- handle the packet (rtu_state passed to allow setting link flag)
|
||||
rtu_comms.handle_packet(msg.message, units, rtu_state)
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if rtu_state.shutdown then
|
||||
rtu_comms.close(rtu_state)
|
||||
log.info("comms thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local rtu_state = smem.rtu_state
|
||||
|
||||
while not rtu_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info("comms thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- per-unit communications handler thread
|
||||
---@param smem rtu_shared_memory
|
||||
---@param unit rtu_unit_registry_entry
|
||||
function threads.thread__unit_comms(smem, unit)
|
||||
local public = {} ---@class thread
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("rtu unit thread start -> " .. unit.type .. "(" .. unit.name .. ")")
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local packet_queue = unit.pkt_queue
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- check for messages in the message queue
|
||||
while packet_queue.ready() and not rtu_state.shutdown do
|
||||
local msg = packet_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
local _, reply = unit.modbus_io.handle_packet(msg.message)
|
||||
rtu_comms.send_modbus(reply)
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if rtu_state.shutdown then
|
||||
log.info("rtu unit thread exiting -> " .. unit.type .. "(" .. unit.name .. ")")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local rtu_state = smem.rtu_state
|
||||
|
||||
while not rtu_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(result)
|
||||
end
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info(util.c("rtu unit thread ", unit.type, "(", unit.name, ") restarting in 5 seconds..."))
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return threads
|
||||
73
scada-common/alarm.lua
Normal file
73
scada-common/alarm.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
---@class alarm
|
||||
local alarm = {}
|
||||
|
||||
---@alias SEVERITY integer
|
||||
SEVERITY = {
|
||||
INFO = 0, -- basic info message
|
||||
WARNING = 1, -- warning about some abnormal state
|
||||
ALERT = 2, -- important device state changes
|
||||
FACILITY = 3, -- facility-wide alert
|
||||
SAFETY = 4, -- safety alerts
|
||||
EMERGENCY = 5 -- critical safety alarm
|
||||
}
|
||||
|
||||
alarm.SEVERITY = SEVERITY
|
||||
|
||||
-- severity integer to string
|
||||
---@param severity SEVERITY
|
||||
function alarm.severity_to_string(severity)
|
||||
if severity == SEVERITY.INFO then
|
||||
return "INFO"
|
||||
elseif severity == SEVERITY.WARNING then
|
||||
return "WARNING"
|
||||
elseif severity == SEVERITY.ALERT then
|
||||
return "ALERT"
|
||||
elseif severity == SEVERITY.FACILITY then
|
||||
return "FACILITY"
|
||||
elseif severity == SEVERITY.SAFETY then
|
||||
return "SAFETY"
|
||||
elseif severity == SEVERITY.EMERGENCY then
|
||||
return "EMERGENCY"
|
||||
else
|
||||
return "UNKNOWN"
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new scada alarm entry
|
||||
---@param severity SEVERITY
|
||||
---@param device string
|
||||
---@param message string
|
||||
function alarm.scada_alarm(severity, device, message)
|
||||
local self = {
|
||||
time = util.time(),
|
||||
ts_string = os.date("[%H:%M:%S]"),
|
||||
severity = severity,
|
||||
device = device,
|
||||
message = message
|
||||
}
|
||||
|
||||
---@class scada_alarm
|
||||
local public = {}
|
||||
|
||||
-- format the alarm as a string
|
||||
---@return string message
|
||||
function public.format()
|
||||
return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device .. ") >> " .. self.message
|
||||
end
|
||||
|
||||
-- get alarm properties
|
||||
function public.properties()
|
||||
return {
|
||||
time = self.time,
|
||||
severity = self.severity,
|
||||
device = self.device,
|
||||
message = self.message
|
||||
}
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return alarm
|
||||
659
scada-common/comms.lua
Normal file
659
scada-common/comms.lua
Normal file
@@ -0,0 +1,659 @@
|
||||
--
|
||||
-- Communications
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
---@class comms
|
||||
local comms = {}
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
local insert = table.insert
|
||||
|
||||
---@alias PROTOCOLS integer
|
||||
local PROTOCOLS = {
|
||||
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
|
||||
RPLC = 1, -- reactor PLC protocol
|
||||
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
|
||||
SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers
|
||||
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
|
||||
}
|
||||
|
||||
---@alias RPLC_TYPES integer
|
||||
local RPLC_TYPES = {
|
||||
LINK_REQ = 0, -- linking requests
|
||||
STATUS = 1, -- reactor/system status
|
||||
MEK_STRUCT = 2, -- mekanism build structure
|
||||
MEK_BURN_RATE = 3, -- set burn rate
|
||||
RPS_ENABLE = 4, -- enable reactor
|
||||
RPS_SCRAM = 5, -- SCRAM reactor
|
||||
RPS_STATUS = 6, -- RPS status
|
||||
RPS_ALARM = 7, -- RPS alarm broadcast
|
||||
RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately)
|
||||
}
|
||||
|
||||
---@alias RPLC_LINKING integer
|
||||
local RPLC_LINKING = {
|
||||
ALLOW = 0, -- link approved
|
||||
DENY = 1, -- link denied
|
||||
COLLISION = 2 -- link denied due to existing active link
|
||||
}
|
||||
|
||||
---@alias SCADA_MGMT_TYPES integer
|
||||
local SCADA_MGMT_TYPES = {
|
||||
KEEP_ALIVE = 0, -- keep alive packet w/ RTT
|
||||
CLOSE = 1, -- close a connection
|
||||
RTU_ADVERT = 2, -- RTU capability advertisement
|
||||
REMOTE_LINKED = 3 -- remote device linked
|
||||
}
|
||||
|
||||
---@alias SCADA_CRDN_TYPES integer
|
||||
local SCADA_CRDN_TYPES = {
|
||||
ESTABLISH = 0, -- initial greeting
|
||||
STRUCT_BUILDS = 1, -- mekanism structure builds
|
||||
UNIT_STATUSES = 2, -- state of reactor units
|
||||
COMMAND_UNIT = 3, -- command a reactor unit
|
||||
ALARM = 4 -- alarm signaling
|
||||
}
|
||||
|
||||
---@alias CAPI_TYPES integer
|
||||
local CAPI_TYPES = {
|
||||
ESTABLISH = 0 -- initial greeting
|
||||
}
|
||||
|
||||
---@alias RTU_UNIT_TYPES integer
|
||||
local RTU_UNIT_TYPES = {
|
||||
REDSTONE = 0, -- redstone I/O
|
||||
BOILER = 1, -- boiler
|
||||
BOILER_VALVE = 2, -- boiler mekanism 10.1+
|
||||
TURBINE = 3, -- turbine
|
||||
TURBINE_VALVE = 4, -- turbine, mekanism 10.1+
|
||||
EMACHINE = 5, -- energy machine
|
||||
IMATRIX = 6, -- induction matrix
|
||||
SPS = 7, -- SPS
|
||||
SNA = 8, -- SNA
|
||||
ENV_DETECTOR = 9 -- environment detector
|
||||
}
|
||||
|
||||
comms.PROTOCOLS = PROTOCOLS
|
||||
comms.RPLC_TYPES = RPLC_TYPES
|
||||
comms.RPLC_LINKING = RPLC_LINKING
|
||||
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
|
||||
comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES
|
||||
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
|
||||
|
||||
-- generic SCADA packet object
|
||||
function comms.scada_packet()
|
||||
local self = {
|
||||
modem_msg_in = nil,
|
||||
valid = false,
|
||||
raw = nil,
|
||||
seq_num = nil,
|
||||
protocol = nil,
|
||||
length = nil,
|
||||
payload = nil
|
||||
}
|
||||
|
||||
---@class scada_packet
|
||||
local public = {}
|
||||
|
||||
-- make a SCADA packet
|
||||
---@param seq_num integer
|
||||
---@param protocol PROTOCOLS
|
||||
---@param payload table
|
||||
function public.make(seq_num, protocol, payload)
|
||||
self.valid = true
|
||||
self.seq_num = seq_num
|
||||
self.protocol = protocol
|
||||
self.length = #payload
|
||||
self.payload = payload
|
||||
self.raw = { self.seq_num, self.protocol, self.payload }
|
||||
end
|
||||
|
||||
-- parse in a modem message as a SCADA packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
self.modem_msg_in = {
|
||||
iface = side,
|
||||
s_port = sender,
|
||||
r_port = reply_to,
|
||||
msg = message,
|
||||
dist = distance
|
||||
}
|
||||
|
||||
self.raw = self.modem_msg_in.msg
|
||||
|
||||
if type(self.raw) == "table" then
|
||||
if #self.raw >= 3 then
|
||||
self.seq_num = self.raw[1]
|
||||
self.protocol = self.raw[2]
|
||||
self.length = #self.raw[3]
|
||||
self.payload = self.raw[3]
|
||||
end
|
||||
|
||||
self.valid = type(self.seq_num) == "number" and
|
||||
type(self.protocol) == "number" and
|
||||
type(self.payload) == "table"
|
||||
end
|
||||
|
||||
return self.valid
|
||||
end
|
||||
|
||||
-- public accessors --
|
||||
|
||||
function public.modem_event() return self.modem_msg_in end
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
function public.local_port() return self.modem_msg_in.s_port end
|
||||
function public.remote_port() return self.modem_msg_in.r_port end
|
||||
|
||||
function public.is_valid() return self.valid end
|
||||
|
||||
function public.seq_num() return self.seq_num end
|
||||
function public.protocol() return self.protocol end
|
||||
function public.length() return self.length end
|
||||
function public.data() return self.payload end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- MODBUS packet
|
||||
-- modeled after MODBUS TCP packet
|
||||
function comms.modbus_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = nil,
|
||||
txn_id = nil,
|
||||
length = nil,
|
||||
unit_id = nil,
|
||||
func_code = nil,
|
||||
data = nil
|
||||
}
|
||||
|
||||
---@class modbus_packet
|
||||
local public = {}
|
||||
|
||||
-- make a MODBUS packet
|
||||
---@param txn_id integer
|
||||
---@param unit_id integer
|
||||
---@param func_code MODBUS_FCODE
|
||||
---@param data table
|
||||
function public.make(txn_id, unit_id, func_code, data)
|
||||
if type(data) == "table" then
|
||||
self.txn_id = txn_id
|
||||
self.length = #data
|
||||
self.unit_id = unit_id
|
||||
self.func_code = func_code
|
||||
self.data = data
|
||||
|
||||
-- populate raw array
|
||||
self.raw = { self.txn_id, self.unit_id, self.func_code }
|
||||
for i = 1, self.length do
|
||||
insert(self.raw, data[i])
|
||||
end
|
||||
else
|
||||
log.error("comms.modbus_packet.make(): data not table")
|
||||
end
|
||||
end
|
||||
|
||||
-- decode a MODBUS packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
local size_ok = frame.length() >= 3
|
||||
|
||||
if size_ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
|
||||
end
|
||||
|
||||
local valid = type(self.txn_id) == "number" and
|
||||
type(self.unit_id) == "number" and
|
||||
type(self.func_code) == "number"
|
||||
|
||||
return size_ok and valid
|
||||
else
|
||||
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
function public.get()
|
||||
---@class modbus_frame
|
||||
local frame = {
|
||||
scada_frame = self.frame,
|
||||
txn_id = self.txn_id,
|
||||
length = self.length,
|
||||
unit_id = self.unit_id,
|
||||
func_code = self.func_code,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- reactor PLC packet
|
||||
function comms.rplc_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = nil,
|
||||
id = nil,
|
||||
type = nil,
|
||||
length = nil,
|
||||
body = nil
|
||||
}
|
||||
|
||||
---@class rplc_packet
|
||||
local public = {}
|
||||
|
||||
-- check that type is known
|
||||
local function _rplc_type_valid()
|
||||
return self.type == RPLC_TYPES.LINK_REQ or
|
||||
self.type == RPLC_TYPES.STATUS or
|
||||
self.type == RPLC_TYPES.MEK_STRUCT or
|
||||
self.type == RPLC_TYPES.MEK_BURN_RATE or
|
||||
self.type == RPLC_TYPES.RPS_ENABLE or
|
||||
self.type == RPLC_TYPES.RPS_SCRAM or
|
||||
self.type == RPLC_TYPES.RPS_ALARM or
|
||||
self.type == RPLC_TYPES.RPS_STATUS or
|
||||
self.type == RPLC_TYPES.RPS_RESET
|
||||
end
|
||||
|
||||
-- make an RPLC packet
|
||||
---@param id integer
|
||||
---@param packet_type RPLC_TYPES
|
||||
---@param data table
|
||||
function public.make(id, packet_type, data)
|
||||
if type(data) == "table" then
|
||||
-- packet accessor properties
|
||||
self.id = id
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = data
|
||||
|
||||
-- populate raw array
|
||||
self.raw = { self.id, self.type }
|
||||
for i = 1, #data do
|
||||
insert(self.raw, data[i])
|
||||
end
|
||||
else
|
||||
log.error("comms.rplc_packet.make(): data not table")
|
||||
end
|
||||
end
|
||||
|
||||
-- decode an RPLC packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.RPLC then
|
||||
local ok = frame.length() >= 2
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], data[2], { table.unpack(data, 3, #data) })
|
||||
ok = _rplc_type_valid()
|
||||
end
|
||||
|
||||
ok = ok and type(self.id) == "number"
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
function public.get()
|
||||
---@class rplc_frame
|
||||
local frame = {
|
||||
scada_frame = self.frame,
|
||||
id = self.id,
|
||||
type = self.type,
|
||||
length = self.length,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- SCADA management packet
|
||||
function comms.mgmt_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = nil,
|
||||
type = nil,
|
||||
length = nil,
|
||||
data = nil
|
||||
}
|
||||
|
||||
---@class mgmt_packet
|
||||
local public = {}
|
||||
|
||||
-- check that type is known
|
||||
local function _scada_type_valid()
|
||||
return self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or
|
||||
self.type == SCADA_MGMT_TYPES.CLOSE or
|
||||
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
|
||||
self.type == SCADA_MGMT_TYPES.RTU_ADVERT
|
||||
end
|
||||
|
||||
-- make a SCADA management packet
|
||||
---@param packet_type SCADA_MGMT_TYPES
|
||||
---@param data table
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
-- packet accessor properties
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = data
|
||||
|
||||
-- populate raw array
|
||||
self.raw = { self.type }
|
||||
for i = 1, #data do
|
||||
insert(self.raw, data[i])
|
||||
end
|
||||
else
|
||||
log.error("comms.mgmt_packet.make(): data not table")
|
||||
end
|
||||
end
|
||||
|
||||
-- decode a SCADA management packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
local ok = frame.length() >= 1
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], { table.unpack(data, 2, #data) })
|
||||
ok = _scada_type_valid()
|
||||
end
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
function public.get()
|
||||
---@class mgmt_frame
|
||||
local frame = {
|
||||
scada_frame = self.frame,
|
||||
type = self.type,
|
||||
length = self.length,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- SCADA coordinator packet
|
||||
function comms.crdn_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = nil,
|
||||
type = nil,
|
||||
length = nil,
|
||||
data = nil
|
||||
}
|
||||
|
||||
---@class crdn_packet
|
||||
local public = {}
|
||||
|
||||
-- check that type is known
|
||||
local function _crdn_type_valid()
|
||||
return self.type == SCADA_CRDN_TYPES.ESTABLISH or
|
||||
self.type == SCADA_CRDN_TYPES.STRUCT_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or
|
||||
self.type == SCADA_CRDN_TYPES.COMMAND_UNIT or
|
||||
self.type == SCADA_CRDN_TYPES.ALARM
|
||||
end
|
||||
|
||||
-- make a coordinator packet
|
||||
---@param packet_type SCADA_CRDN_TYPES
|
||||
---@param data table
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
-- packet accessor properties
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = data
|
||||
|
||||
-- populate raw array
|
||||
self.raw = { self.type }
|
||||
for i = 1, #data do
|
||||
insert(self.raw, data[i])
|
||||
end
|
||||
else
|
||||
log.error("comms.crdn_packet.make(): data not table")
|
||||
end
|
||||
end
|
||||
|
||||
-- decode a coordinator packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.SCADA_CRDN then
|
||||
local ok = frame.length() >= 1
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], { table.unpack(data, 2, #data) })
|
||||
ok = _crdn_type_valid()
|
||||
end
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
function public.get()
|
||||
---@class crdn_frame
|
||||
local frame = {
|
||||
scada_frame = self.frame,
|
||||
type = self.type,
|
||||
length = self.length,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- coordinator API (CAPI) packet
|
||||
-- @todo
|
||||
function comms.capi_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = nil,
|
||||
type = nil,
|
||||
length = nil,
|
||||
data = nil
|
||||
}
|
||||
|
||||
---@class capi_packet
|
||||
local public = {}
|
||||
|
||||
local function _capi_type_valid()
|
||||
-- @todo
|
||||
return false
|
||||
end
|
||||
|
||||
-- make a coordinator API packet
|
||||
---@param packet_type CAPI_TYPES
|
||||
---@param data table
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
-- packet accessor properties
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = data
|
||||
|
||||
-- populate raw array
|
||||
self.raw = { self.type }
|
||||
for i = 1, #data do
|
||||
insert(self.raw, data[i])
|
||||
end
|
||||
else
|
||||
log.error("comms.capi_packet.make(): data not table")
|
||||
end
|
||||
end
|
||||
|
||||
-- decode a coordinator API packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.COORD_API then
|
||||
local ok = frame.length() >= 1
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], { table.unpack(data, 2, #data) })
|
||||
ok = _capi_type_valid()
|
||||
end
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
function public.get()
|
||||
---@class capi_frame
|
||||
local frame = {
|
||||
scada_frame = self.frame,
|
||||
type = self.type,
|
||||
length = self.length,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- convert rtu_t to RTU unit type
|
||||
---@param type rtu_t
|
||||
---@return RTU_UNIT_TYPES|nil
|
||||
function comms.rtu_t_to_unit_type(type)
|
||||
if type == rtu_t.redstone then
|
||||
return RTU_UNIT_TYPES.REDSTONE
|
||||
elseif type == rtu_t.boiler then
|
||||
return RTU_UNIT_TYPES.BOILER
|
||||
elseif type == rtu_t.boiler_valve then
|
||||
return RTU_UNIT_TYPES.BOILER_VALVE
|
||||
elseif type == rtu_t.turbine then
|
||||
return RTU_UNIT_TYPES.TURBINE
|
||||
elseif type == rtu_t.turbine_valve then
|
||||
return RTU_UNIT_TYPES.TURBINE_VALVE
|
||||
elseif type == rtu_t.energy_machine then
|
||||
return RTU_UNIT_TYPES.EMACHINE
|
||||
elseif type == rtu_t.induction_matrix then
|
||||
return RTU_UNIT_TYPES.IMATRIX
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- convert RTU unit type to rtu_t
|
||||
---@param utype RTU_UNIT_TYPES
|
||||
---@return rtu_t|nil
|
||||
function comms.advert_type_to_rtu_t(utype)
|
||||
if utype == RTU_UNIT_TYPES.REDSTONE then
|
||||
return rtu_t.redstone
|
||||
elseif utype == RTU_UNIT_TYPES.BOILER then
|
||||
return rtu_t.boiler
|
||||
elseif utype == RTU_UNIT_TYPES.BOILER_VALVE then
|
||||
return rtu_t.boiler_valve
|
||||
elseif utype == RTU_UNIT_TYPES.TURBINE then
|
||||
return rtu_t.turbine
|
||||
elseif utype == RTU_UNIT_TYPES.TURBINE_VALVE then
|
||||
return rtu_t.turbine_valve
|
||||
elseif utype == RTU_UNIT_TYPES.EMACHINE then
|
||||
return rtu_t.energy_machine
|
||||
elseif utype == RTU_UNIT_TYPES.IMATRIX then
|
||||
return rtu_t.induction_matrix
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return comms
|
||||
245
scada-common/crypto.lua
Normal file
245
scada-common/crypto.lua
Normal file
@@ -0,0 +1,245 @@
|
||||
--
|
||||
-- Cryptographic Communications Engine
|
||||
--
|
||||
|
||||
local aes128 = require("lockbox.cipher.aes128")
|
||||
local ctr_mode = require("lockbox.cipher.mode.ctr");
|
||||
local sha1 = require("lockbox.digest.sha1");
|
||||
local sha2_224 = require("lockbox.digest.sha2_224");
|
||||
local sha2_256 = require("lockbox.digest.sha2_256");
|
||||
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
||||
local hmac = require("lockbox.mac.hmac")
|
||||
local zero_pad = require("lockbox.padding.zero");
|
||||
local stream = require("lockbox.util.stream")
|
||||
local array = require("lockbox.util.array")
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local crypto = {}
|
||||
|
||||
local c_eng = {
|
||||
key = nil,
|
||||
cipher = nil,
|
||||
decipher = nil,
|
||||
hmac = nil
|
||||
}
|
||||
|
||||
---@alias hex string
|
||||
|
||||
-- initialize cryptographic system
|
||||
function crypto.init(password, server_port)
|
||||
local key_deriv = pbkdf2()
|
||||
|
||||
-- setup PBKDF2
|
||||
-- the primary goal is to just turn our password into a 16 byte key
|
||||
key_deriv.setPassword(password)
|
||||
key_deriv.setSalt("salty_salt_at_" .. server_port)
|
||||
key_deriv.setIterations(32)
|
||||
key_deriv.setBlockLen(8)
|
||||
key_deriv.setDKeyLen(16)
|
||||
|
||||
local start = util.time()
|
||||
|
||||
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha2_256))
|
||||
key_deriv.finish()
|
||||
|
||||
log.dmesg("pbkdf2: key derivation took " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
|
||||
|
||||
c_eng.key = array.fromHex(key_deriv.asHex())
|
||||
|
||||
-- initialize cipher
|
||||
c_eng.cipher = ctr_mode.Cipher()
|
||||
c_eng.cipher.setKey(c_eng.key)
|
||||
c_eng.cipher.setBlockCipher(aes128)
|
||||
c_eng.cipher.setPadding(zero_pad);
|
||||
|
||||
-- initialize decipher
|
||||
c_eng.decipher = ctr_mode.Decipher()
|
||||
c_eng.decipher.setKey(c_eng.key)
|
||||
c_eng.decipher.setBlockCipher(aes128)
|
||||
c_eng.decipher.setPadding(zero_pad);
|
||||
|
||||
-- initialize HMAC
|
||||
c_eng.hmac = hmac()
|
||||
c_eng.hmac.setBlockSize(64)
|
||||
c_eng.hmac.setDigest(sha1)
|
||||
c_eng.hmac.setKey(c_eng.key)
|
||||
|
||||
log.dmesg("init: completed in " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
|
||||
end
|
||||
|
||||
-- encrypt plaintext
|
||||
---@param plaintext string
|
||||
---@return string initial_value, string ciphertext
|
||||
function crypto.encrypt(plaintext)
|
||||
local start = util.time()
|
||||
|
||||
-- initial value
|
||||
local iv = {
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255)
|
||||
}
|
||||
|
||||
log.debug("crypto.random: iv random took " .. (util.time() - start) .. "ms")
|
||||
|
||||
start = util.time()
|
||||
|
||||
c_eng.cipher.init()
|
||||
c_eng.cipher.update(stream.fromArray(iv))
|
||||
c_eng.cipher.update(stream.fromString(plaintext))
|
||||
c_eng.cipher.finish()
|
||||
|
||||
local ciphertext = c_eng.cipher.asHex() ---@type hex
|
||||
|
||||
log.debug("crypto.encrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
|
||||
log.debug("ciphertext: " .. util.strval(ciphertext))
|
||||
|
||||
return iv, ciphertext
|
||||
end
|
||||
|
||||
-- decrypt ciphertext
|
||||
---@param iv string CTR initial value
|
||||
---@param ciphertext string ciphertext hex
|
||||
---@return string plaintext
|
||||
function crypto.decrypt(iv, ciphertext)
|
||||
local start = util.time()
|
||||
|
||||
c_eng.decipher.init()
|
||||
c_eng.decipher.update(stream.fromArray(iv))
|
||||
c_eng.decipher.update(stream.fromHex(ciphertext))
|
||||
c_eng.decipher.finish()
|
||||
|
||||
local plaintext_hex = c_eng.decipher.asHex() ---@type hex
|
||||
|
||||
local plaintext = stream.toString(stream.fromHex(plaintext_hex))
|
||||
|
||||
log.debug("crypto.decrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
|
||||
log.debug("plaintext: " .. util.strval(plaintext))
|
||||
|
||||
return plaintext
|
||||
end
|
||||
|
||||
-- generate HMAC of message
|
||||
---@param message_hex string initial value concatenated with ciphertext
|
||||
function crypto.hmac(message_hex)
|
||||
local start = util.time()
|
||||
|
||||
c_eng.hmac.init()
|
||||
c_eng.hmac.update(stream.fromHex(message_hex))
|
||||
c_eng.hmac.finish()
|
||||
|
||||
local hash = c_eng.hmac.asHex() ---@type hex
|
||||
|
||||
log.debug("crypto.hmac: hmac-sha1 took " .. (util.time() - start) .. "ms")
|
||||
log.debug("hmac: " .. util.strval(hash))
|
||||
|
||||
return hash
|
||||
end
|
||||
|
||||
-- wrap a modem as a secure modem to send encrypted traffic
|
||||
---@param modem table modem to wrap
|
||||
function crypto.secure_modem(modem)
|
||||
local self = {
|
||||
modem = modem
|
||||
}
|
||||
|
||||
---@class secure_modem
|
||||
---@field open function
|
||||
---@field isOpen function
|
||||
---@field close function
|
||||
---@field closeAll function
|
||||
---@field isWireless function
|
||||
---@field getNamesRemote function
|
||||
---@field isPresentRemote function
|
||||
---@field getTypeRemote function
|
||||
---@field hasTypeRemote function
|
||||
---@field getMethodsRemote function
|
||||
---@field callRemote function
|
||||
---@field getNameLocal function
|
||||
local public = {}
|
||||
|
||||
-- wrap a modem
|
||||
---@param modem table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.wrap(modem)
|
||||
self.modem = modem
|
||||
for key, func in pairs(self.modem) do
|
||||
public[key] = func
|
||||
end
|
||||
end
|
||||
|
||||
-- wrap modem functions, then we replace transmit
|
||||
public.wrap(self.modem)
|
||||
|
||||
-- send a packet with encryption
|
||||
---@param channel integer
|
||||
---@param reply_channel integer
|
||||
---@param payload table packet raw_sendable
|
||||
function public.transmit(channel, reply_channel, payload)
|
||||
local plaintext = textutils.serialize(payload, { allow_repetitions = true, compact = true })
|
||||
|
||||
local iv, ciphertext = crypto.encrypt(plaintext)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local hmac = crypto.hmac(iv .. ciphertext)
|
||||
|
||||
self.modem.transmit(channel, reply_channel, { hmac, iv, ciphertext })
|
||||
end
|
||||
|
||||
-- parse in a modem message as a network packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any encrypted packet sent with secure_modem.transmit
|
||||
---@param distance integer
|
||||
---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
local body = ""
|
||||
|
||||
if type(message) == "table" then
|
||||
if #message == 3 then
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local hmac = message[1]
|
||||
local iv = message[2]
|
||||
local ciphertext = message[3]
|
||||
|
||||
local computed_hmac = crypto.hmac(iv .. ciphertext)
|
||||
|
||||
if hmac == computed_hmac then
|
||||
-- message intact
|
||||
local plaintext = crypto.decrypt(iv, ciphertext)
|
||||
body = textutils.deserialize(plaintext)
|
||||
|
||||
if body == nil then
|
||||
-- failed decryption
|
||||
log.debug("crypto.secure_modem: decryption failed")
|
||||
body = ""
|
||||
end
|
||||
else
|
||||
-- something went wrong
|
||||
log.debug("crypto.secure_modem: hmac mismatch violation")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return side, sender, reply_to, body, distance
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return crypto
|
||||
310
scada-common/log.lua
Normal file
310
scada-common/log.lua
Normal file
@@ -0,0 +1,310 @@
|
||||
--
|
||||
-- File System Logger
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
---@class log
|
||||
local log = {}
|
||||
|
||||
---@alias MODE integer
|
||||
local MODE = {
|
||||
APPEND = 0,
|
||||
NEW = 1
|
||||
}
|
||||
|
||||
log.MODE = MODE
|
||||
|
||||
-- whether to log debug messages or not
|
||||
local LOG_DEBUG = true
|
||||
|
||||
local _log_sys = {
|
||||
path = "/log.txt",
|
||||
mode = MODE.APPEND,
|
||||
file = nil,
|
||||
dmesg_out = nil
|
||||
}
|
||||
|
||||
---@type function
|
||||
local free_space = fs.getFreeSpace
|
||||
|
||||
-- initialize logger
|
||||
---@param path string file path
|
||||
---@param write_mode MODE
|
||||
---@param dmesg_redirect? table terminal/window to direct dmesg to
|
||||
function log.init(path, write_mode, dmesg_redirect)
|
||||
_log_sys.path = path
|
||||
_log_sys.mode = write_mode
|
||||
|
||||
if _log_sys.mode == MODE.APPEND then
|
||||
_log_sys.file = fs.open(path, "a")
|
||||
else
|
||||
_log_sys.file = fs.open(path, "w")
|
||||
end
|
||||
|
||||
if dmesg_redirect then
|
||||
_log_sys.dmesg_out = dmesg_redirect
|
||||
else
|
||||
_log_sys.dmesg_out = term.current()
|
||||
end
|
||||
end
|
||||
|
||||
-- direct dmesg output to a monitor/window
|
||||
---@param window table window or terminal reference
|
||||
function log.direct_dmesg(window)
|
||||
_log_sys.dmesg_out = window
|
||||
end
|
||||
|
||||
-- private log write function
|
||||
---@param msg string
|
||||
local function _log(msg)
|
||||
local time_stamp = os.date("[%c] ")
|
||||
local stamped = time_stamp .. util.strval(msg)
|
||||
|
||||
-- attempt to write log
|
||||
local status, result = pcall(function ()
|
||||
_log_sys.file.writeLine(stamped)
|
||||
_log_sys.file.flush()
|
||||
end)
|
||||
|
||||
-- if we don't have space, we need to create a new log file
|
||||
|
||||
if not status then
|
||||
if result == "Out of space" then
|
||||
-- will delete log file
|
||||
elseif result ~= nil then
|
||||
util.println("unknown error writing to logfile: " .. result)
|
||||
end
|
||||
end
|
||||
|
||||
if (result == "Out of space") or (free_space(_log_sys.path) < 100) then
|
||||
-- delete the old log file and open a new one
|
||||
_log_sys.file.close()
|
||||
fs.delete(_log_sys.path)
|
||||
log.init(_log_sys.path, _log_sys.mode)
|
||||
|
||||
-- leave a message
|
||||
_log_sys.file.writeLine(time_stamp .. "recycled log file")
|
||||
_log_sys.file.writeLine(stamped)
|
||||
_log_sys.file.flush()
|
||||
end
|
||||
end
|
||||
|
||||
-- dmesg style logging for boot because I like linux-y things
|
||||
---@param msg string message
|
||||
---@param tag? string log tag
|
||||
---@param tag_color? integer log tag color
|
||||
---@return dmesg_ts_coord coordinates line area to place working indicator
|
||||
function log.dmesg(msg, tag, tag_color)
|
||||
---@class dmesg_ts_coord
|
||||
local ts_coord = { x1 = 2, x2 = 3, y = 1 }
|
||||
|
||||
msg = util.strval(msg)
|
||||
tag = tag or ""
|
||||
tag = util.strval(tag)
|
||||
|
||||
local t_stamp = string.format("%12.2f", os.clock())
|
||||
local out = _log_sys.dmesg_out
|
||||
local out_w, out_h = out.getSize()
|
||||
|
||||
local lines = { msg }
|
||||
|
||||
-- wrap if needed
|
||||
if string.len(msg) > out_w then
|
||||
local remaining = true
|
||||
local s_start = 1
|
||||
local s_end = out_w
|
||||
local i = 1
|
||||
|
||||
lines = {}
|
||||
|
||||
while remaining do
|
||||
local line = string.sub(msg, s_start, s_end)
|
||||
|
||||
if line == "" then
|
||||
remaining = false
|
||||
else
|
||||
lines[i] = line
|
||||
|
||||
s_start = s_end + 1
|
||||
s_end = s_end + out_w
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- start output with tag and time, assuming we have enough width for this to be on one line
|
||||
local cur_x, cur_y = out.getCursorPos()
|
||||
|
||||
if cur_x > 1 then
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- colored time
|
||||
local initial_color = out.getTextColor()
|
||||
out.setTextColor(colors.white)
|
||||
out.write("[")
|
||||
out.setTextColor(colors.lightGray)
|
||||
out.write(t_stamp)
|
||||
ts_coord.x2, ts_coord.y = out.getCursorPos()
|
||||
ts_coord.x2 = ts_coord.x2 - 1
|
||||
out.setTextColor(colors.white)
|
||||
out.write("] ")
|
||||
|
||||
-- print optionally colored tag
|
||||
if tag ~= "" then
|
||||
out.write("[")
|
||||
if tag_color then out.setTextColor(tag_color) end
|
||||
out.write(tag)
|
||||
out.setTextColor(colors.white)
|
||||
out.write("] ")
|
||||
end
|
||||
|
||||
out.setTextColor(initial_color)
|
||||
|
||||
-- output message
|
||||
for i = 1, #lines do
|
||||
cur_x, cur_y = out.getCursorPos()
|
||||
|
||||
if i > 1 and cur_x > 1 then
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
out.write(lines[i])
|
||||
end
|
||||
|
||||
_log(util.c("[", t_stamp, "] [", tag, "] ", msg))
|
||||
|
||||
return ts_coord
|
||||
end
|
||||
|
||||
-- print a dmesg message, but then show remaining seconds instead of timestamp
|
||||
---@param msg string message
|
||||
---@param tag? string log tag
|
||||
---@param tag_color? integer log tag color
|
||||
---@return function update, function done
|
||||
function log.dmesg_working(msg, tag, tag_color)
|
||||
local ts_coord = log.dmesg(msg, tag, tag_color)
|
||||
|
||||
local out = _log_sys.dmesg_out
|
||||
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
||||
|
||||
local initial_color = out.getTextColor()
|
||||
|
||||
local counter = 0
|
||||
|
||||
local function update(sec_remaining)
|
||||
local time = util.sprintf("%ds", sec_remaining)
|
||||
local available = width - (string.len(time) + 2)
|
||||
local progress = ""
|
||||
|
||||
out.setCursorPos(ts_coord.x1, ts_coord.y)
|
||||
out.write(" ")
|
||||
|
||||
if counter % 4 == 0 then
|
||||
progress = "|"
|
||||
elseif counter % 4 == 1 then
|
||||
progress = "/"
|
||||
elseif counter % 4 == 2 then
|
||||
progress = "-"
|
||||
elseif counter % 4 == 3 then
|
||||
progress = "\\"
|
||||
end
|
||||
|
||||
out.setTextColor(colors.blue)
|
||||
out.write(progress)
|
||||
out.setTextColor(colors.lightGray)
|
||||
out.write(util.spaces(available) .. time)
|
||||
out.setTextColor(initial_color)
|
||||
|
||||
counter = counter + 1
|
||||
end
|
||||
|
||||
local function done(ok)
|
||||
out.setCursorPos(ts_coord.x1, ts_coord.y)
|
||||
|
||||
if ok or ok == nil then
|
||||
out.setTextColor(colors.green)
|
||||
out.write(util.pad("DONE", width))
|
||||
else
|
||||
out.setTextColor(colors.red)
|
||||
out.write(util.pad("FAIL", width))
|
||||
end
|
||||
|
||||
out.setTextColor(initial_color)
|
||||
end
|
||||
|
||||
return update, done
|
||||
end
|
||||
|
||||
-- log debug messages
|
||||
---@param msg string message
|
||||
---@param trace? boolean include file trace
|
||||
function log.debug(msg, trace)
|
||||
if LOG_DEBUG then
|
||||
local dbg_info = ""
|
||||
|
||||
if trace then
|
||||
local info = debug.getinfo(2)
|
||||
local name = ""
|
||||
|
||||
if info.name ~= nil then
|
||||
name = ":" .. info.name .. "():"
|
||||
end
|
||||
|
||||
dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > "
|
||||
end
|
||||
|
||||
_log("[DBG] " .. dbg_info .. util.strval(msg))
|
||||
end
|
||||
end
|
||||
|
||||
-- log info messages
|
||||
---@param msg string message
|
||||
function log.info(msg)
|
||||
_log("[INF] " .. util.strval(msg))
|
||||
end
|
||||
|
||||
-- log warning messages
|
||||
---@param msg string message
|
||||
function log.warning(msg)
|
||||
_log("[WRN] " .. util.strval(msg))
|
||||
end
|
||||
|
||||
-- log error messages
|
||||
---@param msg string message
|
||||
---@param trace? boolean include file trace
|
||||
function log.error(msg, trace)
|
||||
local dbg_info = ""
|
||||
|
||||
if trace then
|
||||
local info = debug.getinfo(2)
|
||||
local name = ""
|
||||
|
||||
if info.name ~= nil then
|
||||
name = ":" .. info.name .. "():"
|
||||
end
|
||||
|
||||
dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > "
|
||||
end
|
||||
|
||||
_log("[ERR] " .. dbg_info .. util.strval(msg))
|
||||
end
|
||||
|
||||
-- log fatal errors
|
||||
---@param msg string message
|
||||
function log.fatal(msg)
|
||||
_log("[FTL] " .. util.strval(msg))
|
||||
end
|
||||
|
||||
return log
|
||||
83
scada-common/mqueue.lua
Normal file
83
scada-common/mqueue.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
--
|
||||
-- Message Queue
|
||||
--
|
||||
|
||||
local mqueue = {}
|
||||
|
||||
---@alias TYPE integer
|
||||
local TYPE = {
|
||||
COMMAND = 0,
|
||||
DATA = 1,
|
||||
PACKET = 2
|
||||
}
|
||||
|
||||
mqueue.TYPE = TYPE
|
||||
|
||||
-- create a new message queue
|
||||
function mqueue.new()
|
||||
local queue = {}
|
||||
|
||||
local insert = table.insert
|
||||
local remove = table.remove
|
||||
|
||||
---@class queue_item
|
||||
---@field qtype TYPE
|
||||
---@field message any
|
||||
|
||||
---@class queue_data
|
||||
---@field key any
|
||||
---@field val any
|
||||
|
||||
---@class mqueue
|
||||
local public = {}
|
||||
|
||||
-- get queue length
|
||||
function public.length() return #queue end
|
||||
|
||||
-- check if queue is empty
|
||||
---@return boolean is_empty
|
||||
function public.empty() return #queue == 0 end
|
||||
|
||||
-- check if queue has contents
|
||||
function public.ready() return #queue ~= 0 end
|
||||
|
||||
-- push a new item onto the queue
|
||||
---@param qtype TYPE
|
||||
---@param message string
|
||||
local function _push(qtype, message)
|
||||
insert(queue, { qtype = qtype, message = message })
|
||||
end
|
||||
|
||||
-- push a command onto the queue
|
||||
---@param message any
|
||||
function public.push_command(message)
|
||||
_push(TYPE.COMMAND, message)
|
||||
end
|
||||
|
||||
-- push data onto the queue
|
||||
---@param key any
|
||||
---@param value any
|
||||
function public.push_data(key, value)
|
||||
_push(TYPE.DATA, { key = key, val = value })
|
||||
end
|
||||
|
||||
-- push a packet onto the queue
|
||||
---@param packet scada_packet|modbus_packet|rplc_packet|crdn_packet|capi_packet
|
||||
function public.push_packet(packet)
|
||||
_push(TYPE.PACKET, packet)
|
||||
end
|
||||
|
||||
-- get an item off the queue
|
||||
---@return queue_item|nil
|
||||
function public.pop()
|
||||
if #queue > 0 then
|
||||
return remove(queue, 1)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return mqueue
|
||||
336
scada-common/ppm.lua
Normal file
336
scada-common/ppm.lua
Normal file
@@ -0,0 +1,336 @@
|
||||
--
|
||||
-- Protected Peripheral Manager
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
---@class ppm
|
||||
local ppm = {}
|
||||
|
||||
local ACCESS_FAULT = nil ---@type nil
|
||||
|
||||
ppm.ACCESS_FAULT = ACCESS_FAULT
|
||||
|
||||
----------------------------
|
||||
-- PRIVATE DATA/FUNCTIONS --
|
||||
----------------------------
|
||||
|
||||
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
|
||||
|
||||
local _ppm_sys = {
|
||||
mounts = {},
|
||||
auto_cf = false,
|
||||
faulted = false,
|
||||
last_fault = "",
|
||||
terminate = false,
|
||||
mute = false
|
||||
}
|
||||
|
||||
-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program
|
||||
---
|
||||
---also provides peripheral-specific fault checks (auto-clear fault defaults to true)
|
||||
---
|
||||
---assumes iface is a valid peripheral
|
||||
---@param iface string CC peripheral interface
|
||||
local function peri_init(iface)
|
||||
local self = {
|
||||
faulted = false,
|
||||
last_fault = "",
|
||||
fault_counts = {},
|
||||
auto_cf = true,
|
||||
type = peripheral.getType(iface),
|
||||
device = peripheral.wrap(iface)
|
||||
}
|
||||
|
||||
-- initialization process (re-map)
|
||||
|
||||
for key, func in pairs(self.device) do
|
||||
self.fault_counts[key] = 0
|
||||
self.device[key] = function (...)
|
||||
local return_table = table.pack(pcall(func, ...))
|
||||
|
||||
local status = return_table[1]
|
||||
table.remove(return_table, 1)
|
||||
|
||||
if status then
|
||||
-- auto fault clear
|
||||
if self.auto_cf then self.faulted = false end
|
||||
if _ppm_sys.auto_cf then _ppm_sys.faulted = false end
|
||||
|
||||
self.fault_counts[key] = 0
|
||||
|
||||
return table.unpack(return_table)
|
||||
else
|
||||
local result = return_table[1]
|
||||
|
||||
-- function failed
|
||||
self.faulted = true
|
||||
self.last_fault = result
|
||||
|
||||
_ppm_sys.faulted = true
|
||||
_ppm_sys.last_fault = result
|
||||
|
||||
if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
count_str = " [" .. self.fault_counts[key] .. " total faults]"
|
||||
end
|
||||
|
||||
log.error(util.c("PPM: protected ", key, "() -> ", result, count_str))
|
||||
end
|
||||
|
||||
self.fault_counts[key] = self.fault_counts[key] + 1
|
||||
|
||||
if result == "Terminated" then
|
||||
_ppm_sys.terminate = true
|
||||
end
|
||||
|
||||
return ACCESS_FAULT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- fault management functions
|
||||
|
||||
local function clear_fault() self.faulted = false end
|
||||
local function get_last_fault() return self.last_fault end
|
||||
local function is_faulted() return self.faulted end
|
||||
local function is_ok() return not self.faulted end
|
||||
|
||||
local function enable_afc() self.auto_cf = true end
|
||||
local function disable_afc() self.auto_cf = false end
|
||||
|
||||
-- append to device functions
|
||||
|
||||
self.device.__p_clear_fault = clear_fault
|
||||
self.device.__p_last_fault = get_last_fault
|
||||
self.device.__p_is_faulted = is_faulted
|
||||
self.device.__p_is_ok = is_ok
|
||||
self.device.__p_enable_afc = enable_afc
|
||||
self.device.__p_disable_afc = disable_afc
|
||||
|
||||
return {
|
||||
type = self.type,
|
||||
dev = self.device
|
||||
}
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- PUBLIC FUNCTIONS --
|
||||
----------------------
|
||||
|
||||
-- REPORTING --
|
||||
|
||||
-- silence error prints
|
||||
function ppm.disable_reporting()
|
||||
_ppm_sys.mute = true
|
||||
end
|
||||
|
||||
-- allow error prints
|
||||
function ppm.enable_reporting()
|
||||
_ppm_sys.mute = false
|
||||
end
|
||||
|
||||
-- FAULT MEMORY --
|
||||
|
||||
-- enable automatically clearing fault flag
|
||||
function ppm.enable_afc()
|
||||
_ppm_sys.auto_cf = true
|
||||
end
|
||||
|
||||
-- disable automatically clearing fault flag
|
||||
function ppm.disable_afc()
|
||||
_ppm_sys.auto_cf = false
|
||||
end
|
||||
|
||||
-- clear fault flag
|
||||
function ppm.clear_fault()
|
||||
_ppm_sys.faulted = false
|
||||
end
|
||||
|
||||
-- check fault flag
|
||||
function ppm.is_faulted()
|
||||
return _ppm_sys.faulted
|
||||
end
|
||||
|
||||
-- get the last fault message
|
||||
function ppm.get_last_fault()
|
||||
return _ppm_sys.last_fault
|
||||
end
|
||||
|
||||
-- TERMINATION --
|
||||
|
||||
-- if a caught error was a termination request
|
||||
function ppm.should_terminate()
|
||||
return _ppm_sys.terminate
|
||||
end
|
||||
|
||||
-- MOUNTING --
|
||||
|
||||
-- mount all available peripherals (clears mounts first)
|
||||
function ppm.mount_all()
|
||||
local ifaces = peripheral.getNames()
|
||||
|
||||
_ppm_sys.mounts = {}
|
||||
|
||||
for i = 1, #ifaces do
|
||||
_ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
|
||||
log.info(util.c("PPM: found a ", _ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
end
|
||||
|
||||
if #ifaces == 0 then
|
||||
log.warning("PPM: mount_all() -> no devices found")
|
||||
end
|
||||
end
|
||||
|
||||
-- mount a particular device
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type, table|nil device
|
||||
function ppm.mount(iface)
|
||||
local ifaces = peripheral.getNames()
|
||||
local pm_dev = nil
|
||||
local pm_type = nil
|
||||
|
||||
for i = 1, #ifaces do
|
||||
if iface == ifaces[i] then
|
||||
_ppm_sys.mounts[iface] = peri_init(iface)
|
||||
|
||||
pm_type = _ppm_sys.mounts[iface].type
|
||||
pm_dev = _ppm_sys.mounts[iface].dev
|
||||
|
||||
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
|
||||
-- handle peripheral_detach event
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type, table|nil device
|
||||
function ppm.handle_unmount(iface)
|
||||
local pm_dev = nil
|
||||
local pm_type = nil
|
||||
|
||||
-- what got disconnected?
|
||||
local lost_dev = _ppm_sys.mounts[iface]
|
||||
|
||||
if lost_dev then
|
||||
pm_type = lost_dev.type
|
||||
pm_dev = lost_dev.dev
|
||||
|
||||
log.warning(util.c("PPM: lost device ", pm_type, " mounted to ", iface))
|
||||
else
|
||||
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
|
||||
end
|
||||
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
|
||||
-- GENERAL ACCESSORS --
|
||||
|
||||
-- list all available peripherals
|
||||
---@return table names
|
||||
function ppm.list_avail()
|
||||
return peripheral.getNames()
|
||||
end
|
||||
|
||||
-- list mounted peripherals
|
||||
---@return table mounts
|
||||
function ppm.list_mounts()
|
||||
return _ppm_sys.mounts
|
||||
end
|
||||
|
||||
-- get a mounted peripheral by side/interface
|
||||
---@param iface string CC peripheral interface
|
||||
---@return table|nil device function table
|
||||
function ppm.get_periph(iface)
|
||||
if _ppm_sys.mounts[iface] then
|
||||
return _ppm_sys.mounts[iface].dev
|
||||
else return nil end
|
||||
end
|
||||
|
||||
-- get a mounted peripheral type by side/interface
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type
|
||||
function ppm.get_type(iface)
|
||||
if _ppm_sys.mounts[iface] then
|
||||
return _ppm_sys.mounts[iface].type
|
||||
else return nil end
|
||||
end
|
||||
|
||||
-- get all mounted peripherals by type
|
||||
---@param name string type name
|
||||
---@return table devices device function tables
|
||||
function ppm.get_all_devices(name)
|
||||
local devices = {}
|
||||
|
||||
for _, data in pairs(_ppm_sys.mounts) do
|
||||
if data.type == name then
|
||||
table.insert(devices, data.dev)
|
||||
end
|
||||
end
|
||||
|
||||
return devices
|
||||
end
|
||||
|
||||
-- get a mounted peripheral by type (if multiple, returns the first)
|
||||
---@param name string type name
|
||||
---@return table|nil device function table
|
||||
function ppm.get_device(name)
|
||||
local device = nil
|
||||
|
||||
for side, data in pairs(_ppm_sys.mounts) do
|
||||
if data.type == name then
|
||||
device = data.dev
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return device
|
||||
end
|
||||
|
||||
-- SPECIFIC DEVICE ACCESSORS --
|
||||
|
||||
-- get the fission reactor (if multiple, returns the first)
|
||||
---@return table|nil reactor function table
|
||||
function ppm.get_fission_reactor()
|
||||
return ppm.get_device("fissionReactor") or ppm.get_device("fissionReactorLogicAdapter")
|
||||
end
|
||||
|
||||
-- get the wireless modem (if multiple, returns the first)
|
||||
--
|
||||
-- if this is in a CraftOS emulated environment, wired modems will be used instead
|
||||
---@return table|nil modem function table
|
||||
function ppm.get_wireless_modem()
|
||||
local w_modem = nil
|
||||
local emulated_env = periphemu ~= nil
|
||||
|
||||
for _, device in pairs(_ppm_sys.mounts) do
|
||||
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
|
||||
w_modem = device.dev
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return w_modem
|
||||
end
|
||||
|
||||
-- list all connected monitors
|
||||
---@return table monitors
|
||||
function ppm.get_monitor_list()
|
||||
local list = {}
|
||||
|
||||
for iface, device in pairs(_ppm_sys.mounts) do
|
||||
if device.type == "monitor" then
|
||||
list[iface] = device
|
||||
end
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
return ppm
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user