Compare commits
218 Commits
v1.10.0-be
...
devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae37f53c0 | ||
|
|
dcd04b36c2 | ||
|
|
e73a0a2d5b | ||
|
|
d35f43d575 | ||
|
|
3bf7f9cbd8 | ||
|
|
5e58d7482f | ||
|
|
83adad1ef6 | ||
|
|
7973d80486 | ||
|
|
6dcb6febdf | ||
|
|
5d90f879db | ||
|
|
522292d5df | ||
|
|
9b3842345c | ||
|
|
8bfd218fef | ||
|
|
2e423e641b | ||
|
|
ba1909e13f | ||
|
|
2abd212f28 | ||
|
|
308308d56f | ||
|
|
99239a135a | ||
|
|
1dbc89d4ce | ||
|
|
7ca0e9481d | ||
|
|
8781a063e6 | ||
|
|
50f830efa3 | ||
|
|
a5b134d446 | ||
|
|
0a6e33ac17 | ||
|
|
3e018aa412 | ||
|
|
3eefac4551 | ||
|
|
28e87fd8fa | ||
|
|
bc83a1b577 | ||
|
|
d408ca8748 | ||
|
|
ee29fbc4b6 | ||
|
|
f3dfcbd0cb | ||
|
|
b9b7b323dc | ||
|
|
fb5f339204 | ||
|
|
b39d52606c | ||
|
|
88975add1b | ||
|
|
30eb757889 | ||
|
|
dd11afe5cb | ||
|
|
18cadcf8ca | ||
|
|
dfda8b5d1a | ||
|
|
8929786315 | ||
|
|
1aafa8b574 | ||
|
|
6272473c95 | ||
|
|
311b8d19d1 | ||
|
|
d80c7a5826 | ||
|
|
0e5825a821 | ||
|
|
61c931d709 | ||
|
|
25d31b8a0c | ||
|
|
6399301f9c | ||
|
|
d65750c282 | ||
|
|
d3d62fb924 | ||
|
|
c69da9aaaf | ||
|
|
7eb96bb4c8 | ||
|
|
28bb4cd6bb | ||
|
|
74e19436a8 | ||
|
|
d348728604 | ||
|
|
15e55eb7ab | ||
|
|
b9ad688bbb | ||
|
|
d734c66f69 | ||
|
|
5330ecb6c2 | ||
|
|
69baf85748 | ||
|
|
7fa748e9dd | ||
|
|
c65ce8bf3b | ||
|
|
f10d5a0c7d | ||
|
|
f25c37383e | ||
|
|
c6150f342f | ||
|
|
91c41e91eb | ||
|
|
7424e48fd1 | ||
|
|
9be07a6eb1 | ||
|
|
a8278a94f6 | ||
|
|
2ccb720b07 | ||
|
|
e2bb78d09c | ||
|
|
804ff24b65 | ||
|
|
ffa74f82c6 | ||
|
|
cd140d7882 | ||
|
|
874bda7db6 | ||
|
|
cfce5802b1 | ||
|
|
f98fe207d2 | ||
|
|
ef63b3df5d | ||
|
|
cfe53ffaad | ||
|
|
be8cf1e295 | ||
|
|
89f5d15e53 | ||
|
|
330c876ba5 | ||
|
|
c9a1bfb925 | ||
|
|
9a9fafdad0 | ||
|
|
687965ff22 | ||
|
|
c4cf1a3917 | ||
|
|
d9e008c1cc | ||
|
|
608768bcb7 | ||
|
|
fcad27bbfa | ||
|
|
1ae98eff69 | ||
|
|
88c11d644e | ||
|
|
6b8a174661 | ||
|
|
8989e90b7a | ||
|
|
53edc25947 | ||
|
|
6abc0669d8 | ||
|
|
d12190b601 | ||
|
|
13b05de87b | ||
|
|
1e0eeb4d57 | ||
|
|
6b737dc9e4 | ||
|
|
889f01231e | ||
|
|
45986c859f | ||
|
|
adb4decdaf | ||
|
|
2182379bd4 | ||
|
|
cac75c2edb | ||
|
|
db1b90df90 | ||
|
|
ebfdec2294 | ||
|
|
68ae061ed3 | ||
|
|
7f951a985b | ||
|
|
965fa0c7d1 | ||
|
|
99742c621a | ||
|
|
983724f45f | ||
|
|
d43112bdeb | ||
|
|
4dabc3f0aa | ||
|
|
2e01b478f1 | ||
|
|
fb102502dd | ||
|
|
2b015759fd | ||
|
|
82aff3b30b | ||
|
|
12d688daec | ||
|
|
d4c5140003 | ||
|
|
21c36c70be | ||
|
|
b020dde122 | ||
|
|
fa551f0b4f | ||
|
|
29513bac38 | ||
|
|
1254d668a9 | ||
|
|
3987d337c4 | ||
|
|
76fc5751c9 | ||
|
|
6e26ca4fac | ||
|
|
32af935e9e | ||
|
|
46b23414b0 | ||
|
|
3f1cf217ac | ||
|
|
6dea501946 | ||
|
|
cc36aafccd | ||
|
|
569358a4e1 | ||
|
|
504dce64c2 | ||
|
|
699ba9f71e | ||
|
|
c0f9ba6ba6 | ||
|
|
138c10ad1f | ||
|
|
6774f60605 | ||
|
|
2ecb662b0a | ||
|
|
50dedaa7c8 | ||
|
|
55e4fed9d8 | ||
|
|
b9a9c018a1 | ||
|
|
e0a0c34b54 | ||
|
|
25207f39c0 | ||
|
|
8c8d3faf72 | ||
|
|
9ff183b17d | ||
|
|
212e1f8fe8 | ||
|
|
b2baaa2090 | ||
|
|
44340f42d4 | ||
|
|
61305621c3 | ||
|
|
6123d5dad7 | ||
|
|
5abe687f69 | ||
|
|
299c6bcf7a | ||
|
|
7745e94fbe | ||
|
|
9a58bf1bb7 | ||
|
|
6bfa26407a | ||
|
|
645c2bacbd | ||
|
|
802ef149c5 | ||
|
|
cc9d5fe2d6 | ||
|
|
d9001090c2 | ||
|
|
0417986c15 | ||
|
|
4c8d5bc4a0 | ||
|
|
8bbf385c41 | ||
|
|
d7a280bb04 | ||
|
|
f88dc0b5b9 | ||
|
|
34eb16df00 | ||
|
|
0cc62b3447 | ||
|
|
cddd9f7437 | ||
|
|
5acc6470e3 | ||
|
|
e57c6205e2 | ||
|
|
96acb03f73 | ||
|
|
2fefe4fbd6 | ||
|
|
1f9e86f6ea | ||
|
|
a48c8c1efe | ||
|
|
deeeb612b1 | ||
|
|
c62eaeb5a2 | ||
|
|
1a7cb9eaa8 | ||
|
|
3139dc2176 | ||
|
|
25fc0050c3 | ||
|
|
b57aceff15 | ||
|
|
8fd04e44f3 | ||
|
|
869b342db2 | ||
|
|
f0251efec6 | ||
|
|
c7e02efbc7 | ||
|
|
db8bed583f | ||
|
|
2d44014e2e | ||
|
|
452fe71ab8 | ||
|
|
22208e91aa | ||
|
|
d412f61a5f | ||
|
|
c62ec1e786 | ||
|
|
1890f0a983 | ||
|
|
390cf98b0a | ||
|
|
7ddd6f32c5 | ||
|
|
18a488f1b9 | ||
|
|
4c7ad0c539 | ||
|
|
a083f8983b | ||
|
|
cb11ece73d | ||
|
|
fc24f39991 | ||
|
|
4d6c388f37 | ||
|
|
9e3922a972 | ||
|
|
1fcc91e98b | ||
|
|
fe9ee313f9 | ||
|
|
194a266730 | ||
|
|
88862726e3 | ||
|
|
2aa5c93404 | ||
|
|
c6d526163f | ||
|
|
859e04712f | ||
|
|
9591668f87 | ||
|
|
4a38ca7dd1 | ||
|
|
250db00794 | ||
|
|
391b68d357 | ||
|
|
4a7fc6200e | ||
|
|
bee1cdf01c | ||
|
|
c6143934d8 | ||
|
|
c319039a4e | ||
|
|
4b61037170 | ||
|
|
028a161af0 | ||
|
|
454d166ac9 |
57
CONTRIBUTING.md
Normal file
57
CONTRIBUTING.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Contribution Guide
|
||||
|
||||
>[!NOTE]
|
||||
Until the system is out of beta, contributions will be limited as I wrap up the specific release feature set.
|
||||
|
||||
This project is highly complex for a ComputerCraft Lua application. Contributions need to follow style guides and meet the code quality I've kept this project up to for years. Contributions must be tested appropriately with test results included.
|
||||
|
||||
I have extensively tested software components for stability required for safety, with tiers of software robustness.
|
||||
1. **Critical: High-Impact** -
|
||||
The Reactor-PLC is "uncrashable" and must remain so. I've extensively reviewed every line and behavior, so any code contributions must be at this high standard. Simple is stable, so the less code the better. Always check for parameter validity and extensively test any changes to critical thread functions.
|
||||
2. **Important: Moderate-Impact** -
|
||||
The Supervisor and RTU Gateway should rarely, if ever, crash. Certain places may not be held to as strict of a level as above, but should be written understanding all the possible inputs to and impacts of a section of code.
|
||||
3. **Useful: Low-Impact** -
|
||||
The Coordinator and Pocket are nice UI apps, and things can break. There's a lot of data going to and from them, so checking every single incoming value would have negative performance impacts and increase program size. If they break, the user can restart them. Don't introduce careless bugs, but making assumptions about the integrity of incoming data is acceptable.
|
||||
|
||||
## Valuable Contributions
|
||||
|
||||
Pull requests should not consist of purely whitespace changes, comment changes, or other trivial changes. They should target specific features, bug fixes, or functional improvements. I reserve the right to decline PRs that don't follow this in good faith.
|
||||
|
||||
## Project Management Guidelines
|
||||
|
||||
Any contributions should be linked to an open GitHub issue. These are used to track progress, discuss changes, etc. Surprise changes to this project might conflict with existing plans, so I prefer we coordinate changes ahead of time.
|
||||
|
||||
## Software Guidelines
|
||||
|
||||
These guidelines are subject to change. The general rule is make the code look like the rest of the code around it and elsewhere in the project.
|
||||
|
||||
### Style Guide
|
||||
|
||||
PRs will only be accepted if they match the style of this project and pass manual and automated code analysis. Listing out the whole style guide would take a while, so as stated above, please review code adjacent to your modifications.
|
||||
|
||||
1. **No Block Comments.**
|
||||
These interfere with the minification used for the bundled installation files due to the complexity of parsing Lua block comments. The minification code is meant to be simple to have 0 risk of breaking anything, so I'm staying far away from those.
|
||||
2. **Comment Your Code.**
|
||||
This includes type hints as used elsewhere throughout the project. Your comments should be associated with parts of code that are more complex or unclear, or otherwise to split up sections of tasks. You'll see `--#region` used in various places.
|
||||
- Type hints are intended to be utilized by the `sumneko.lua` vscode extension. You should use this while developing, as it provides extremely valuable functionality.
|
||||
3. **Whitespace Usage.**
|
||||
Whitespace should be used to separate function parameters and operators. The one exception is the unique styling of graphics elements, which you should compare against if modifying them.
|
||||
- 4 spaces are used for all indentation.
|
||||
- Try to align assignment operator lines as is done elsewhere (adding space before `=`).
|
||||
- Use empty new lines to separate steps or distinct groups of operations.
|
||||
- Generally add new lines for each step in loops and for statements. For some single-line ones, they may be compressed into a single line. This saves on space utilization, especially on deeply indented lines.
|
||||
4. **Variables and Classes.**
|
||||
- Variables, functions, and class-like tables follow the snake_case convention.
|
||||
- Graphics objects and configuration settings follow PascalCase.
|
||||
- Constants follow all-caps SNAKE_CASE and local ones should be declared at the top of files after `require` statements and external ones (like `local ALARM = types.ALARM`).
|
||||
5. **No `goto`.**
|
||||
These are generally frowned upon due to reducing code readability.
|
||||
6. **Multiple `return`s.**
|
||||
These are allowed to minimize code size, but if it is simple to avoid multiple, do so.
|
||||
7. **Classes and Objects.**
|
||||
Review the existing code for examples on how objects are implemented in this project. They do not use Lua's `:` operator and `self` functionality. A manual object-like table definition is used. Some global single-instance classes don't use a `new()` function, such as the [PPM](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/scada-common/ppm.lua). Multi-instance ones do, such as the Supervisor's [unit](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/supervisor/unit.lua) class.
|
||||
|
||||
### No AI
|
||||
|
||||
Your code should follow the style guide, be succinct, make sense, and you should be able to explain what it does. Random changes done in multiple places will be deemed suspicious along with poor comments or nonsensical code.
|
||||
Use your contributions as programming practice or to hone your skills; don't automate away thinking.
|
||||
514
coordinator/backplane.lua
Normal file
514
coordinator/backplane.lua
Normal file
@@ -0,0 +1,514 @@
|
||||
--
|
||||
-- Coordinator System Core Peripheral Backplane
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local println = util.println
|
||||
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
|
||||
---@class crd_backplane
|
||||
local backplane = {}
|
||||
|
||||
local _bp = {
|
||||
smem = nil, ---@type crd_shared_memory
|
||||
|
||||
wlan_pref = true,
|
||||
lan_iface = "",
|
||||
|
||||
act_nic = nil, ---@type nic
|
||||
wd_nic = nil, ---@type nic|nil
|
||||
wl_nic = nil, ---@type nic|nil
|
||||
nic_map = {}, ---@type nic[] connected nics
|
||||
|
||||
speaker = nil, ---@type Speaker|nil
|
||||
|
||||
---@class crd_displays
|
||||
displays = {
|
||||
main = nil, ---@type Monitor|nil
|
||||
main_iface = "",
|
||||
flow = nil, ---@type Monitor|nil
|
||||
flow_iface = "",
|
||||
unit_displays = {}, ---@type Monitor[]
|
||||
unit_ifaces = {} ---@type string[]
|
||||
}
|
||||
}
|
||||
|
||||
-- network interfaces indexed by peripheral names
|
||||
backplane.nics = _bp.nic_map
|
||||
|
||||
-- initialize the display peripheral backplane
|
||||
---@param config crd_config
|
||||
---@return boolean success, string error_msg
|
||||
function backplane.init_displays(config)
|
||||
local displays = _bp.displays
|
||||
|
||||
local w, h, _
|
||||
|
||||
log.info("BKPLN: DISPLAY INIT")
|
||||
|
||||
-- monitor configuration verification
|
||||
|
||||
local mon_cfv = util.new_validator()
|
||||
|
||||
mon_cfv.assert_type_str(config.MainDisplay)
|
||||
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
|
||||
|
||||
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
|
||||
for i = 1, #config.UnitDisplays do
|
||||
mon_cfv.assert_type_str(config.UnitDisplays[i])
|
||||
end
|
||||
|
||||
if not mon_cfv.valid() then
|
||||
return false, "Monitor configuration invalid."
|
||||
end
|
||||
|
||||
-- setup and check display peripherals
|
||||
|
||||
-- main display
|
||||
|
||||
local disp, iface = ppm.get_periph(config.MainDisplay), config.MainDisplay
|
||||
|
||||
displays.main = disp
|
||||
displays.main_iface = iface
|
||||
|
||||
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " MAIN/" .. iface)
|
||||
|
||||
ioctl.fp_monitor_state("main", util.trinary(disp, 2, 1))
|
||||
|
||||
if not disp then
|
||||
return false, "Main monitor is not connected."
|
||||
end
|
||||
|
||||
disp.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(disp.getSize())
|
||||
if w ~= 8 then
|
||||
log.info("BKPLN: DISPLAY MAIN/" .. iface .. " BAD RESOLUTION")
|
||||
return false, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
|
||||
-- flow display
|
||||
|
||||
if not config.DisableFlowView then
|
||||
disp, iface = ppm.get_periph(config.FlowDisplay), config.FlowDisplay
|
||||
|
||||
displays.flow = disp
|
||||
displays.flow_iface = iface
|
||||
|
||||
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " FLOW/" .. iface)
|
||||
|
||||
ioctl.fp_monitor_state("flow", util.trinary(disp, 2, 1))
|
||||
|
||||
if not disp then
|
||||
return false, "Flow monitor is not connected."
|
||||
end
|
||||
|
||||
disp.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(disp.getSize())
|
||||
if w ~= 8 then
|
||||
log.info("BKPLN: DISPLAY FLOW/" .. iface .. " BAD RESOLUTION")
|
||||
return false, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
end
|
||||
|
||||
-- unit display(s)
|
||||
|
||||
for i = 1, config.UnitCount do
|
||||
disp, iface = ppm.get_periph(config.UnitDisplays[i]), config.UnitDisplays[i]
|
||||
|
||||
displays.unit_displays[i] = disp
|
||||
displays.unit_ifaces[i] = iface
|
||||
|
||||
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " UNIT_" .. i .. "/" .. iface)
|
||||
|
||||
ioctl.fp_monitor_state(i, util.trinary(disp, 2, 1))
|
||||
|
||||
if not disp then
|
||||
return false, "Unit " .. i .. " monitor is not connected."
|
||||
end
|
||||
|
||||
disp.setTextScale(0.5)
|
||||
w, h = ppm.monitor_block_size(disp.getSize())
|
||||
if w ~= 4 or h ~= 4 then
|
||||
log.info("BKPLN: DISPLAY UNIT_" .. i .. "/" .. iface .. " BAD RESOLUTION")
|
||||
return false, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
|
||||
end
|
||||
end
|
||||
|
||||
log.info("BKPLN: DISPLAY INIT OK")
|
||||
|
||||
return true, ""
|
||||
end
|
||||
|
||||
-- initialize the system peripheral backplane
|
||||
---@param config crd_config
|
||||
---@param __shared_memory crd_shared_memory
|
||||
---@return boolean success
|
||||
function backplane.init(config, __shared_memory)
|
||||
_bp.smem = __shared_memory
|
||||
_bp.wlan_pref = config.PreferWireless
|
||||
_bp.lan_iface = config.WiredModem
|
||||
|
||||
-- Modem Init
|
||||
|
||||
-- init wired NIC
|
||||
if type(_bp.lan_iface) == "string" then
|
||||
local modem = ppm.get_modem(_bp.lan_iface)
|
||||
local wd_nic = network.nic(modem, config.SVR_Channel)
|
||||
|
||||
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
|
||||
log_comms("wired comms modem " .. util.trinary(modem, "connected", "not found"))
|
||||
|
||||
_bp.wd_nic = wd_nic
|
||||
_bp.act_nic = wd_nic -- set this as active for now
|
||||
_bp.nic_map[_bp.lan_iface] = wd_nic
|
||||
|
||||
wd_nic.closeAll()
|
||||
wd_nic.open(config.CRD_Channel)
|
||||
|
||||
ioctl.fp_has_wd_modem(modem ~= nil)
|
||||
end
|
||||
|
||||
-- init wireless NIC(s)
|
||||
if config.WirelessModem then
|
||||
local modem, iface = ppm.get_wireless_modem()
|
||||
local wl_nic = network.nic(modem, config.SVR_Channel)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
|
||||
log_comms("wireless comms modem " .. util.trinary(modem, "connected", "not found"))
|
||||
|
||||
-- set this as active if connected or if both modems are disconnected and this is preferred
|
||||
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
|
||||
_bp.act_nic = wl_nic
|
||||
log.info("BKPLN: switched active to preferred wireless")
|
||||
end
|
||||
|
||||
_bp.wl_nic = wl_nic
|
||||
if iface then _bp.nic_map[iface] = wl_nic end
|
||||
|
||||
wl_nic.closeAll()
|
||||
wl_nic.open(config.CRD_Channel)
|
||||
|
||||
ioctl.fp_has_wl_modem(modem ~= nil)
|
||||
end
|
||||
|
||||
-- at least one comms modem is required
|
||||
if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then
|
||||
log_comms("no comms modem found")
|
||||
println("startup> no comms modem found")
|
||||
log.warning("BKPLN: no comms modem on startup")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Speaker Init
|
||||
|
||||
_bp.speaker = ppm.get_device("speaker")
|
||||
|
||||
if not _bp.speaker then
|
||||
log_boot("annunciator alarm speaker not found")
|
||||
|
||||
println("startup> speaker not found")
|
||||
log.fatal("BKPLN: no annunciator alarm speaker found")
|
||||
|
||||
return false
|
||||
else
|
||||
log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(_bp.speaker))
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
|
||||
local sounder_start = util.time_ms()
|
||||
sounder.init(_bp.speaker, config.SpeakerVolume)
|
||||
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
|
||||
ioctl.fp_has_speaker(true)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- get the active NIC
|
||||
function backplane.active_nic() return _bp.act_nic end
|
||||
|
||||
-- get the standby NIC
|
||||
---@return nic|nil
|
||||
function backplane.standby_nic() return util.trinary(_bp.act_nic == _bp.wl_nic, _bp.wd_nic, _bp.wl_nic) end
|
||||
|
||||
-- get the wireless NIC
|
||||
function backplane.wireless_nic() return _bp.wl_nic end
|
||||
|
||||
-- get the configured displays
|
||||
function backplane.displays() return _bp.displays end
|
||||
|
||||
-- periodic backplane peripheral tasks
|
||||
function backplane.periodic()
|
||||
if _bp.wd_nic then ioctl.fp_has_wd_net(_bp.wd_nic.periodic()) end
|
||||
if _bp.wl_nic then ioctl.fp_has_wl_net(_bp.wl_nic.periodic()) end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral attach
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param iface string
|
||||
function backplane.attach(type, device, iface)
|
||||
local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA
|
||||
|
||||
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
|
||||
|
||||
local comms = _bp.smem.crd_sys.coord_comms
|
||||
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
local m_is_wl = device.isWireless()
|
||||
|
||||
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
|
||||
|
||||
if wd_nic and (_bp.lan_iface == iface) then
|
||||
-- connect this as the wired NIC
|
||||
wd_nic.connect(device)
|
||||
_bp.nic_map[iface] = wd_nic
|
||||
|
||||
log.info("BKPLN: WIRED PHY_UP " .. iface)
|
||||
log_sys("wired comms modem reconnected")
|
||||
|
||||
ioctl.fp_has_wd_modem(true)
|
||||
|
||||
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
|
||||
-- switch back to preferred wired
|
||||
_bp.act_nic = wd_nic
|
||||
|
||||
comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wired modem (preferred)")
|
||||
end
|
||||
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
|
||||
-- connect this as the wireless NIC
|
||||
wl_nic.connect(device)
|
||||
_bp.nic_map[iface] = wl_nic
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
|
||||
log_sys("wireless comms modem reconnected")
|
||||
|
||||
ioctl.fp_has_wl_modem(true)
|
||||
|
||||
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
|
||||
-- switch back to preferred wireless
|
||||
_bp.act_nic = wl_nic
|
||||
|
||||
comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wireless modem (preferred)")
|
||||
end
|
||||
elseif wl_nic and m_is_wl then
|
||||
-- the wireless NIC already has a modem
|
||||
device.closeAll()
|
||||
|
||||
log_sys("standby wireless modem connected")
|
||||
log.info("BKPLN: standby wireless modem connected")
|
||||
else
|
||||
device.closeAll()
|
||||
|
||||
log_sys("unassigned modem connected")
|
||||
log.warning("BKPLN: unassigned modem connected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@cast device Monitor
|
||||
|
||||
local is_used = false
|
||||
|
||||
log.info("BKPLN: DISPLAY LINK_UP " .. iface)
|
||||
|
||||
if _bp.displays.main_iface == iface then
|
||||
is_used = true
|
||||
|
||||
_bp.displays.main = device
|
||||
|
||||
log.info("BKPLN: main display reconnected")
|
||||
ioctl.fp_monitor_state("main", 2)
|
||||
elseif _bp.displays.flow_iface == iface then
|
||||
is_used = true
|
||||
|
||||
_bp.displays.flow = device
|
||||
|
||||
log.info("BKPLN: flow display reconnected")
|
||||
ioctl.fp_monitor_state("flow", 2)
|
||||
else
|
||||
for idx, monitor in ipairs(_bp.displays.unit_ifaces) do
|
||||
if monitor == iface then
|
||||
is_used = true
|
||||
|
||||
_bp.displays.unit_displays[idx] = device
|
||||
|
||||
log.info("BKPLN: unit " .. idx .. " display reconnected")
|
||||
ioctl.fp_monitor_state(idx, 2)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- notify renderer if it is using it
|
||||
if is_used then
|
||||
log_sys(util.c("configured monitor ", iface, " reconnected"))
|
||||
_bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, iface)
|
||||
else
|
||||
log_sys(util.c("unused monitor ", iface, " connected"))
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
|
||||
log.info("BKPLN: SPEAKER LINK_UP " .. iface)
|
||||
|
||||
sounder.reconnect(device)
|
||||
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
|
||||
ioctl.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral detach
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param iface string
|
||||
function backplane.detach(type, device, iface)
|
||||
local MQ__RENDER_CMD = _bp.smem.q_types.MQ__RENDER_CMD
|
||||
local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA
|
||||
|
||||
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
|
||||
|
||||
local comms = _bp.smem.crd_sys.coord_comms
|
||||
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
log.info(util.c("BKPLN: PHY_DETACH ", iface))
|
||||
|
||||
_bp.nic_map[iface] = nil
|
||||
|
||||
if wd_nic and wd_nic.is_modem(device) then
|
||||
wd_nic.disconnect()
|
||||
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
|
||||
|
||||
ioctl.fp_has_wd_modem(false)
|
||||
elseif wl_nic and wl_nic.is_modem(device) then
|
||||
wl_nic.disconnect()
|
||||
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
|
||||
|
||||
ioctl.fp_has_wl_modem(false)
|
||||
end
|
||||
|
||||
-- we only care if this is our active comms modem
|
||||
if _bp.act_nic.is_modem(device) then
|
||||
log_sys("active comms modem disconnected")
|
||||
log.warning("BKPLN: active comms modem disconnected")
|
||||
|
||||
-- failover and try to find a new comms modem
|
||||
if _bp.act_nic == wl_nic then
|
||||
-- wireless active disconnected
|
||||
-- try to find another wireless modem, otherwise switch to wired
|
||||
local modem, m_iface = ppm.get_wireless_modem()
|
||||
if wl_nic and modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
log.info("BKPLN: found another wireless modem, using it for comms")
|
||||
|
||||
wl_nic.connect(modem)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
|
||||
|
||||
ioctl.fp_has_wl_modem(true)
|
||||
elseif wd_nic and wd_nic.is_connected() then
|
||||
_bp.act_nic = wd_nic
|
||||
|
||||
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wired modem")
|
||||
else
|
||||
-- close out main UI
|
||||
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
comms.close()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
end
|
||||
elseif wl_nic and wl_nic.is_connected() then
|
||||
-- wired active disconnected, wireless available
|
||||
_bp.act_nic = wl_nic
|
||||
|
||||
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wireless modem")
|
||||
else
|
||||
-- wired active disconnected, wireless unavailable
|
||||
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
comms.close()
|
||||
end
|
||||
elseif wd_nic and wd_nic.is_modem(device) then
|
||||
-- wired, but not active
|
||||
log_sys("standby wired modem disconnected")
|
||||
log.info("BKPLN: standby wired modem disconnected")
|
||||
elseif wl_nic and wl_nic.is_modem(device) then
|
||||
-- wireless, but not active
|
||||
log_sys("standby wireless modem disconnected")
|
||||
log.info("BKPLN: standby wireless modem disconnected")
|
||||
else
|
||||
log_sys("unassigned modem disconnected")
|
||||
log.warning("BKPLN: unassigned modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@cast device Monitor
|
||||
|
||||
local is_used = false
|
||||
|
||||
log.info("BKPLN: DISPLAY LINK_DOWN " .. iface)
|
||||
|
||||
if _bp.displays.main == device then
|
||||
is_used = true
|
||||
|
||||
log.info("BKPLN: main display disconnected")
|
||||
ioctl.fp_monitor_state("main", 1)
|
||||
elseif _bp.displays.flow == device then
|
||||
is_used = true
|
||||
|
||||
log.info("BKPLN: flow display disconnected")
|
||||
ioctl.fp_monitor_state("flow", 1)
|
||||
else
|
||||
for idx, monitor in pairs(_bp.displays.unit_displays) do
|
||||
if monitor == device then
|
||||
is_used = true
|
||||
|
||||
log.info("BKPLN: unit " .. idx .. " display disconnected")
|
||||
ioctl.fp_monitor_state(idx, 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- notify renderer if it was using it
|
||||
if is_used then
|
||||
log_sys("lost a configured monitor")
|
||||
_bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, iface)
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
|
||||
log.info("BKPLN: SPEAKER LINK_DOWN " .. iface)
|
||||
|
||||
log_sys("alarm sounder speaker disconnected")
|
||||
|
||||
ioctl.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return backplane
|
||||
@@ -51,18 +51,17 @@ local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min an
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function send_sv(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
self.nic.transmit(self.tmp_cfg.SVR_Channel, self.tmp_cfg.CRD_Channel, s_pkt)
|
||||
self.nic.transmit(self.tmp_cfg.SVR_Channel, self.tmp_cfg.CRD_Channel, frame)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- handle an establish message from the supervisor
|
||||
---@param packet mgmt_frame
|
||||
---@param packet mgmt_packet
|
||||
local function handle_packet(packet)
|
||||
local error_msg = nil
|
||||
|
||||
@@ -149,18 +148,39 @@ local function handle_timeout()
|
||||
end
|
||||
|
||||
-- attempt a connection to the supervisor to get cooling info
|
||||
local function sv_connect()
|
||||
---@param cfg crd_config current configuration for modem settings
|
||||
local function sv_connect(cfg)
|
||||
self.sv_conn_button.disable()
|
||||
self.sv_conn_detail.set_value("")
|
||||
|
||||
local modem = ppm.get_wireless_modem()
|
||||
local modem = nil
|
||||
|
||||
if cfg.WirelessModem then
|
||||
modem = ppm.get_wireless_modem()
|
||||
|
||||
if cfg.WiredModem then
|
||||
local wd_modem = ppm.get_modem(cfg.WiredModem)
|
||||
|
||||
if cfg.PreferWireless then
|
||||
if not modem then
|
||||
modem = wd_modem
|
||||
end
|
||||
else
|
||||
if wd_modem then
|
||||
modem = wd_modem
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif cfg.WiredModem then
|
||||
modem = ppm.get_modem(cfg.WiredModem)
|
||||
end
|
||||
|
||||
if modem == nil then
|
||||
self.sv_conn_status.set_value("Please connect an ender/wireless modem.")
|
||||
self.sv_conn_status.set_value("Could not find configured modem(s).")
|
||||
else
|
||||
self.sv_conn_status.set_value("Modem found, connecting...")
|
||||
if self.nic == nil then self.nic = network.nic(modem) end
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic = network.nic(modem)
|
||||
self.nic.open(self.tmp_cfg.CRD_Channel)
|
||||
|
||||
self.sv_addr = comms.BROADCAST
|
||||
@@ -199,17 +219,17 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
|
||||
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}}
|
||||
local fac_pane = MultiPane{parent=fac_cfg,y=4,panes={fac_c_1,fac_c_2,fac_c_3}}
|
||||
|
||||
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
|
||||
TextBox{parent=fac_cfg,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."}
|
||||
TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."}
|
||||
TextBox{parent=fac_c_1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."}
|
||||
TextBox{parent=fac_c_1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."}
|
||||
|
||||
self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
|
||||
self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
|
||||
self.sv_conn_detail = TextBox{parent=fac_c_1,y=11,height=2,text=""}
|
||||
|
||||
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
self.sv_conn_button = PushButton{parent=fac_c_1,y=9,text="Connect",min_width=9,callback=function()sv_connect(tmp_cfg)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
local function sv_skip()
|
||||
tcd.abort(handle_timeout)
|
||||
@@ -224,15 +244,15 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
fac_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
self.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true}
|
||||
|
||||
TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
|
||||
tool_ctl.num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=fac_c_2,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
|
||||
tool_ctl.num_units = NumberField{parent=fac_c_2,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=fac_c_2,x=7,y=5,text="reactors"}
|
||||
TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=fac_c_2,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
TextBox{parent=fac_c_2,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
@@ -246,14 +266,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
else nu_error.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_2,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."}
|
||||
TextBox{parent=fac_c_3,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."}
|
||||
|
||||
local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local fac_config_list = ListBox{parent=fac_c_3,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_3,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -303,13 +323,13 @@ end
|
||||
---@param distance integer
|
||||
function facility.receive_sv(side, sender, reply_to, message, distance)
|
||||
if self.nic ~= nil and self.net_listen then
|
||||
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
local frame = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
if frame and frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local pkt = comms.mgmt_container().decode(frame)
|
||||
if pkt then
|
||||
tcd.abort(handle_timeout)
|
||||
handle_packet(mgmt_pkt.get())
|
||||
handle_packet(pkt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,12 +52,12 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49}
|
||||
local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49}
|
||||
|
||||
local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}}
|
||||
local mon_pane = MultiPane{parent=mon_cfg,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}}
|
||||
|
||||
TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)}
|
||||
TextBox{parent=mon_cfg,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)}
|
||||
|
||||
TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."}
|
||||
local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
TextBox{parent=mon_c_1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."}
|
||||
local mon_reqs = ListBox{parent=mon_c_1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function next_from_reqs()
|
||||
-- unassign unit monitors above the unit count
|
||||
@@ -67,13 +67,13 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
mon_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."}
|
||||
TextBox{parent=mon_c_2,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."}
|
||||
|
||||
local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local mon_list = ListBox{parent=mon_c_2,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
@@ -98,14 +98,14 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
assign_err.show()
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_2,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""}
|
||||
local mon_desc = TextBox{parent=mon_c_3,y=1,height=4,text=""}
|
||||
|
||||
local mon_unit_l, mon_unit = nil, nil ---@type TextBox, NumberField
|
||||
|
||||
local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
local mon_warn = TextBox{parent=mon_c_3,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
|
||||
---@param val integer assignment type
|
||||
local function on_assign_mon(val)
|
||||
@@ -135,8 +135,8 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
if value == "0" or value == nil then mon_unit.set_value(0) end
|
||||
end
|
||||
|
||||
TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"}
|
||||
local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue}
|
||||
TextBox{parent=mon_c_3,y=6,width=10,text="Assignment"}
|
||||
local mon_assign = RadioButton{parent=mon_c_3,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue}
|
||||
|
||||
mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"}
|
||||
mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
@@ -181,13 +181,13 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
mon_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=mon_c_3,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."}
|
||||
TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."}
|
||||
TextBox{parent=mon_c_4,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."}
|
||||
TextBox{parent=mon_c_4,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."}
|
||||
|
||||
tool_ctl.dis_flow_view = Checkbox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)}
|
||||
tool_ctl.dis_flow_view = Checkbox{parent=mon_c_4,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)}
|
||||
|
||||
local function back_from_legacy()
|
||||
tmp_cfg.DisableFlowView = tool_ctl.dis_flow_view.get_value()
|
||||
@@ -203,14 +203,14 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
|
||||
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
|
||||
TextBox{parent=spkr_cfg,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
|
||||
|
||||
TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."}
|
||||
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
|
||||
TextBox{parent=spkr_c,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."}
|
||||
TextBox{parent=spkr_c,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
|
||||
|
||||
tool_ctl.s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
|
||||
tool_ctl.s_vol = NumberField{parent=spkr_c,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=spkr_c,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
@@ -223,7 +223,7 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
else s_vol_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=spkr_c,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -232,22 +232,21 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
|
||||
local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
|
||||
TextBox{parent=crd_cfg,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
|
||||
|
||||
TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."}
|
||||
TextBox{parent=crd_c_1,y=1,height=2,text="You can customize the UI with the interface options below."}
|
||||
|
||||
TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
|
||||
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
TextBox{parent=crd_c_1,y=4,text="Clock Time Format"}
|
||||
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
|
||||
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
|
||||
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
TextBox{parent=crd_c_1,y=8,text="Temperature Scale"}
|
||||
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=crd_c_1,x=20,y=8,text="Energy Scale"}
|
||||
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
local function submit_ui_opts()
|
||||
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1
|
||||
@@ -257,7 +256,7 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
main_pane.set_value(7)
|
||||
end
|
||||
|
||||
PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=crd_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -296,13 +295,13 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
|
||||
mon_reqs.remove_all()
|
||||
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=mon_reqs,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
|
||||
TextBox{parent=mon_reqs,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=mon_reqs,y=1,text="\x1a 1 Main View Monitor"}
|
||||
TextBox{parent=mon_reqs,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
if not tmp_cfg.DisableFlowView then
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"}
|
||||
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=mon_reqs,y=1,text="\x1a 1 Flow View Monitor"}
|
||||
TextBox{parent=mon_reqs,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -402,9 +401,9 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
end
|
||||
end
|
||||
|
||||
local line = Div{parent=mon_list,x=1,y=1,height=1}
|
||||
local line = Div{parent=mon_list,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
|
||||
TextBox{parent=line,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
|
||||
TextBox{parent=line,x=8,y=1,text=iface}
|
||||
|
||||
local w, h = ppm.monitor_block_size(dev.getSize())
|
||||
@@ -431,9 +430,9 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
|
||||
|
||||
-- add monitors that are assigned but not connected
|
||||
for i = 1, #dc_list do
|
||||
local line = Div{parent=mon_list,x=1,y=1,height=1}
|
||||
local line = Div{parent=mon_list,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
|
||||
TextBox{parent=line,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
|
||||
TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)}
|
||||
|
||||
local function unset_mon()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
@@ -31,6 +32,9 @@ local RIGHT = core.ALIGN.RIGHT
|
||||
local self = {
|
||||
importing_legacy = false,
|
||||
|
||||
api_en = nil, ---@type Checkbox
|
||||
pkt_chan = nil, ---@type NumberField
|
||||
api_timeout = nil, ---@type NumberField
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
@@ -63,100 +67,204 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_5 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_6 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
local net_pane = MultiPane{parent=net_cfg,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4,net_c_5,net_c_6}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
TextBox{parent=net_cfg,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,y=1,text="Please select the network interface(s)."}
|
||||
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local function on_wired_change(_) tool_ctl.gen_modem_list() end
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local wireless = Checkbox{parent=net_c_1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black)}
|
||||
TextBox{parent=net_c_1,x=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
|
||||
local wired = Checkbox{parent=net_c_1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
|
||||
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
|
||||
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
|
||||
local modem_list = ListBox{parent=net_c_1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local function submit_interfaces()
|
||||
tmp_cfg.WirelessModem = wireless.get_value()
|
||||
|
||||
if not wired.get_value() then
|
||||
tmp_cfg.WiredModem = false
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
if not (wired.get_value() or wireless.get_value()) then
|
||||
modem_err.set_value("Please select a modem type.")
|
||||
modem_err.show()
|
||||
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
|
||||
modem_err.set_value("Please select a wired modem.")
|
||||
modem_err.show()
|
||||
else
|
||||
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
|
||||
self.wl_pref.enable()
|
||||
else
|
||||
self.wl_pref.set_value(tmp_cfg.WirelessModem)
|
||||
self.wl_pref.disable()
|
||||
end
|
||||
|
||||
if not tmp_cfg.WirelessModem then
|
||||
self.api_en.set_value(false)
|
||||
self.api_en.disable()
|
||||
else
|
||||
self.api_en.enable()
|
||||
end
|
||||
|
||||
net_pane.set_value(2)
|
||||
modem_err.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,text="If you selected multiple interfaces, please specify if this device should prefer wireless or otherwise wired. The preferred interface is switched too when reconnected even if failover has succeeded onto the fallback interface."}
|
||||
self.wl_pref = Checkbox{parent=net_c_2,y=7,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=19,y=7,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
|
||||
TextBox{parent=net_c_2,y=9,text="With a wireless modem, configure Pocket access."}
|
||||
self.api_en = Checkbox{parent=net_c_2,y=11,label="Enable Pocket Access",default=ini_cfg.API_Enabled,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=24,y=11,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
|
||||
local function submit_net_cfg_opts()
|
||||
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
|
||||
tmp_cfg.PreferWireless = self.wl_pref.get_value()
|
||||
else
|
||||
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
|
||||
end
|
||||
|
||||
tmp_cfg.API_Enabled = tri(tmp_cfg.WirelessModem, self.api_en.get_value(), false)
|
||||
|
||||
if tmp_cfg.API_Enabled then
|
||||
self.pkt_chan.enable()
|
||||
self.api_timeout.enable()
|
||||
else
|
||||
self.pkt_chan.disable()
|
||||
self.api_timeout.disable()
|
||||
end
|
||||
|
||||
net_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_net_cfg_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_3,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_3,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=10,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_3,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=12,width=14,text="Pocket Channel"}
|
||||
self.pkt_chan = NumberField{parent=net_c_3,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=net_c_3,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(self.pkt_chan.get_value())
|
||||
|
||||
if not tmp_cfg.API_Enabled then pkt_c = tmp_cfg.PKT_Channel or 16244 end
|
||||
|
||||
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
|
||||
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
|
||||
net_pane.set_value(2)
|
||||
net_pane.set_value(4)
|
||||
chan_err.hide(true)
|
||||
else chan_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,y=1,text="Please set the connection timeouts below."}
|
||||
TextBox{parent=net_c_4,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
|
||||
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=8,width=19,text="Supervisor Timeout"}
|
||||
local svr_timeout = NumberField{parent=net_c_4,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"}
|
||||
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=10,width=14,text="Pocket Timeout"}
|
||||
self.api_timeout = NumberField{parent=net_c_4,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
|
||||
TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local ct_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
|
||||
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(self.api_timeout.get_value())
|
||||
|
||||
if not tmp_cfg.API_Enabled then api_cto = tmp_cfg.API_Timeout or 5 end
|
||||
|
||||
if svr_cto ~= nil and api_cto ~= nil then
|
||||
tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto
|
||||
net_pane.set_value(3)
|
||||
|
||||
if tmp_cfg.WirelessModem then
|
||||
net_pane.set_value(5)
|
||||
else
|
||||
tmp_cfg.TrustedRange = 0
|
||||
tmp_cfg.AuthKey = ""
|
||||
|
||||
network.deinit_mac()
|
||||
|
||||
-- prep supervisor connection screen
|
||||
tool_ctl.init_sv_connect_ui()
|
||||
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
|
||||
ct_err.hide(true)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_5,y=1,text="Please set the wireless trusted range below."}
|
||||
TextBox{parent=net_c_5,y=3,height=3,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_5,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
local range = NumberField{parent=net_c_5,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local tr_err = TextBox{parent=net_c_5,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_tr()
|
||||
local range_val = tonumber(range.get_value())
|
||||
if range_val ~= nil then
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
comms.set_trusted_range(range_val)
|
||||
net_pane.set_value(4)
|
||||
net_pane.set_value(6)
|
||||
tr_err.hide(true)
|
||||
else tr_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_5,y=14,text="\x1b Back",callback=function()net_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_5,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_6,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_6,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_6,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
|
||||
local key, _ = TextField{parent=net_c_6,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
|
||||
|
||||
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
local hide_key = Checkbox{parent=net_c_6,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local key_err = TextBox{parent=net_c_6,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
@@ -174,8 +282,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_6,y=14,text="\x1b Back",callback=function()net_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_6,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -183,17 +291,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
TextBox{parent=log_cfg,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
|
||||
TextBox{parent=log_c_1,y=1,text="Please configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
TextBox{parent=log_c_1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=log_c_1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
local en_dbg = Checkbox{parent=log_c_1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
@@ -210,7 +318,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -222,20 +330,20 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
local clr_pane = MultiPane{parent=clr_cfg,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
TextBox{parent=clr_cfg,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."}
|
||||
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=clr_c_1,y=1,height=2,text="Here you can select the color themes for the different UI displays."}
|
||||
TextBox{parent=clr_c_1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
|
||||
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_1,y=7,text="Main UI Theme"}
|
||||
local main_theme = RadioButton{parent=clr_c_1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
|
||||
TextBox{parent=clr_c_2,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
@@ -264,8 +372,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_2,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
@@ -308,7 +416,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
@@ -320,12 +428,12 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
clr_pane.set_value(1)
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_3,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -337,11 +445,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
local sum_pane = MultiPane{parent=summary,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
TextBox{parent=summary,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local setting_list = ListBox{parent=sum_c_1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_summary()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
@@ -370,11 +478,15 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(wireless, ini_cfg.WirelessModem)
|
||||
try_set(wired, ini_cfg.WiredModem ~= false)
|
||||
try_set(self.wl_pref, ini_cfg.PreferWireless)
|
||||
try_set(self.api_en, ini_cfg.API_Enabled)
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(self.pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(svr_timeout, ini_cfg.SVR_Timeout)
|
||||
try_set(api_timeout, ini_cfg.API_Timeout)
|
||||
try_set(self.api_timeout, ini_cfg.API_Timeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
|
||||
@@ -409,11 +521,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_1,y=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_2,y=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
@@ -424,10 +536,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."}
|
||||
TextBox{parent=sum_c_3,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/coordinator/config.lua")
|
||||
@@ -435,11 +547,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=sum_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
@@ -565,7 +677,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
textbox = TextBox{parent=line,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
@@ -574,6 +686,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
-- generate the list of available/assigned wired modems
|
||||
function tool_ctl.gen_modem_list()
|
||||
modem_list.remove_all()
|
||||
|
||||
local enable = wired.get_value()
|
||||
|
||||
local function select(iface)
|
||||
tmp_cfg.WiredModem = iface
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
local modems = ppm.get_wired_modem_list()
|
||||
local missing = { tmp = true, ini = true }
|
||||
|
||||
for iface, _ in pairs(modems) do
|
||||
if ini_cfg.WiredModem == iface then missing.ini = false end
|
||||
if tmp_cfg.WiredModem == iface then missing.tmp = false end
|
||||
end
|
||||
|
||||
if missing.tmp and tmp_cfg.WiredModem then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
|
||||
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
|
||||
end
|
||||
|
||||
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
|
||||
-- list wired modems
|
||||
for iface, _ in pairs(modems) do
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == iface
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text=iface}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ local changes = {
|
||||
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
|
||||
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
|
||||
{ "v1.5.1", { "Added energy scale options" } },
|
||||
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } }
|
||||
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } },
|
||||
{ "v1.7.0", { "Added support for wired communications modems", "Added option for allowing Pocket connections" } }
|
||||
}
|
||||
|
||||
---@class crd_configurator
|
||||
@@ -89,7 +90,9 @@ local tool_ctl = {
|
||||
is_int_min_max = nil, ---@type function
|
||||
|
||||
update_mon_reqs = nil, ---@type function
|
||||
gen_mon_list = function () end
|
||||
|
||||
gen_mon_list = function () end,
|
||||
gen_modem_list = function () end
|
||||
}
|
||||
|
||||
---@class crd_config
|
||||
@@ -104,6 +107,10 @@ local tmp_cfg = {
|
||||
MainDisplay = nil, ---@type string
|
||||
FlowDisplay = nil, ---@type string
|
||||
UnitDisplays = {}, ---@type string[]
|
||||
WirelessModem = true,
|
||||
WiredModem = false, ---@type string|false
|
||||
PreferWireless = true,
|
||||
API_Enabled = true,
|
||||
SVR_Channel = nil, ---@type integer
|
||||
CRD_Channel = nil, ---@type integer
|
||||
PKT_Channel = nil, ---@type integer
|
||||
@@ -136,6 +143,10 @@ local fields = {
|
||||
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
|
||||
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
|
||||
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },
|
||||
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
|
||||
{ "WiredModem", "Wired Comms Modem", false },
|
||||
{ "PreferWireless", "Prefer Wireless Modem", true },
|
||||
{ "API_Enabled", "Pocket API Connectivity", true },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
@@ -189,20 +200,20 @@ local function config_view(display)
|
||||
|
||||
TextBox{parent=display,y=1,text="Coordinator Configurator",alignment=CENTER,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
local root_pane_div = Div{parent=display,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local fac_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local mon_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local spkr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local crd_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
local main_page = Div{parent=root_pane_div,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,y=1}
|
||||
local fac_cfg = Div{parent=root_pane_div,y=1}
|
||||
local mon_cfg = Div{parent=root_pane_div,y=1}
|
||||
local spkr_cfg = Div{parent=root_pane_div,y=1}
|
||||
local crd_cfg = Div{parent=root_pane_div,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,y=1}
|
||||
local summary = Div{parent=root_pane_div,y=1}
|
||||
local changelog = Div{parent=root_pane_div,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,fac_cfg,mon_cfg,spkr_cfg,crd_cfg,log_cfg,clr_cfg,summary,changelog}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,y=1,panes={main_page,net_cfg,fac_cfg,mon_cfg,spkr_cfg,crd_cfg,log_cfg,clr_cfg,summary,changelog}}
|
||||
|
||||
--#region Main Page
|
||||
|
||||
@@ -287,20 +298,20 @@ local function config_view(display)
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=changelog,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local c_log = ListBox{parent=cl,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=cl,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
end
|
||||
@@ -327,6 +338,9 @@ function configurator.configure(start_code, message)
|
||||
-- copy in some important values to start with
|
||||
preset_monitor_fields()
|
||||
|
||||
-- this needs to be initialized as it is used before being set
|
||||
tmp_cfg.WiredModem = ini_cfg.WiredModem
|
||||
|
||||
reset_term()
|
||||
|
||||
ppm.mount_all()
|
||||
@@ -341,6 +355,7 @@ function configurator.configure(start_code, message)
|
||||
config_view(display)
|
||||
|
||||
tool_ctl.gen_mon_list()
|
||||
tool_ctl.gen_modem_list()
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
@@ -364,8 +379,10 @@ function configurator.configure(start_code, message)
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.mount(param1)
|
||||
tool_ctl.gen_mon_list()
|
||||
tool_ctl.gen_modem_list()
|
||||
elseif event == "monitor_resize" then
|
||||
tool_ctl.gen_mon_list()
|
||||
tool_ctl.gen_modem_list()
|
||||
elseif event == "modem_message" then
|
||||
facility.receive_sv(param1, param2, param3, param4, param5)
|
||||
end
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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 types = require("scada-common.types")
|
||||
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
@@ -21,6 +20,10 @@ local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_TIMEOUT = 60.0
|
||||
|
||||
-- wait 5 seconds after initializing a network switch request before being allowed to send more,
|
||||
-- which avoids repeat duplicate requests
|
||||
local FAILOVER_GRACE_PERIOD_MS = 5000
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
---@type crd_config
|
||||
@@ -29,11 +32,9 @@ local config = {}
|
||||
|
||||
coordinator.config = config
|
||||
|
||||
-- load the coordinator configuration<br>
|
||||
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
|
||||
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
|
||||
-- load the coordinator configuration
|
||||
function coordinator.load_config()
|
||||
if not settings.load("/coordinator.settings") then return 1 end
|
||||
if not settings.load("/coordinator.settings") then return false end
|
||||
|
||||
config.UnitCount = settings.get("UnitCount")
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
@@ -47,6 +48,10 @@ function coordinator.load_config()
|
||||
config.FlowDisplay = settings.get("FlowDisplay")
|
||||
config.UnitDisplays = settings.get("UnitDisplays")
|
||||
|
||||
config.WirelessModem = settings.get("WirelessModem")
|
||||
config.WiredModem = settings.get("WiredModem")
|
||||
config.PreferWireless = settings.get("PreferWireless")
|
||||
config.API_Enabled = settings.get("API_Enabled")
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
@@ -80,6 +85,13 @@ function coordinator.load_config()
|
||||
cfv.assert_type_num(config.SpeakerVolume)
|
||||
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_type_bool(config.WirelessModem)
|
||||
cfv.assert((config.WiredModem == false) or (type(config.WiredModem) == "string"))
|
||||
cfv.assert(config.WirelessModem or (type(config.WiredModem) == "string"))
|
||||
cfv.assert_type_bool(config.PreferWireless)
|
||||
|
||||
cfv.assert_type_bool(config.API_Enabled)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
cfv.assert_channel(config.PKT_Channel)
|
||||
@@ -110,85 +122,7 @@ function coordinator.load_config()
|
||||
cfv.assert_type_int(config.ColorMode)
|
||||
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
|
||||
-- Monitor Setup
|
||||
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
main = nil, ---@type Monitor|nil
|
||||
main_name = "",
|
||||
flow = nil, ---@type Monitor|nil
|
||||
flow_name = "",
|
||||
unit_displays = {}, ---@type Monitor[]
|
||||
unit_name_map = {} ---@type string[]
|
||||
}
|
||||
|
||||
local mon_cfv = util.new_validator()
|
||||
|
||||
-- get all interface names
|
||||
local names = {}
|
||||
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
|
||||
|
||||
local function setup_monitors()
|
||||
mon_cfv.assert_type_str(config.MainDisplay)
|
||||
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
|
||||
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
|
||||
|
||||
if mon_cfv.valid() then
|
||||
local w, h, _
|
||||
|
||||
if not util.table_contains(names, config.MainDisplay) then
|
||||
return 2, "Main monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.main = ppm.get_periph(config.MainDisplay)
|
||||
monitors.main_name = config.MainDisplay
|
||||
|
||||
monitors.main.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.main.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
|
||||
if not config.DisableFlowView then
|
||||
if not util.table_contains(names, config.FlowDisplay) then
|
||||
return 2, "Flow monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.flow = ppm.get_periph(config.FlowDisplay)
|
||||
monitors.flow_name = config.FlowDisplay
|
||||
|
||||
monitors.flow.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, config.UnitCount do
|
||||
local display = config.UnitDisplays[i]
|
||||
if type(display) ~= "string" or not util.table_contains(names, display) then
|
||||
return 2, "Unit " .. i .. " monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.unit_displays[i] = ppm.get_periph(display)
|
||||
monitors.unit_name_map[i] = display
|
||||
|
||||
monitors.unit_displays[i].setTextScale(0.5)
|
||||
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
|
||||
if w ~= 4 or h ~= 4 then
|
||||
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
|
||||
end
|
||||
end
|
||||
else return 2, "Monitor configuration invalid." end
|
||||
end
|
||||
|
||||
if cfv.valid() then
|
||||
local ok, result, message = pcall(setup_monitors)
|
||||
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
|
||||
if result == 2 then return 2, message end
|
||||
else return 1 end
|
||||
|
||||
return 0, monitors
|
||||
return cfv.valid()
|
||||
end
|
||||
|
||||
-- dmesg print wrapper
|
||||
@@ -232,15 +166,16 @@ end
|
||||
-- coordinator communications
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param nic nic network interface device
|
||||
---@param backplane crd_backplane coordinator backplane
|
||||
---@param sv_watchdog watchdog
|
||||
function coordinator.comms(version, nic, sv_watchdog)
|
||||
function coordinator.comms(version, backplane, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
|
||||
sv_r_seq_num = nil, ---@type nil|integer
|
||||
sv_config_err = false,
|
||||
failover_init = 0,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
last_api_est_acks = {},
|
||||
est_start = 0,
|
||||
@@ -249,58 +184,65 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
est_task_done = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
local tx_nic = backplane.active_nic()
|
||||
local wl_nic = backplane.wireless_nic()
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(config.CRD_Channel)
|
||||
if config.WirelessModem then
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
end
|
||||
|
||||
-- pass config to apisessions
|
||||
apisessions.init(nic, config)
|
||||
if config.API_Enabled and wl_nic then
|
||||
apisessions.init(wl_nic, config)
|
||||
end
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
--#region PRIVATE FUNCTIONS --
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt ---@type mgmt_packet|crdn_packet
|
||||
local frame = comms.scada_frame()
|
||||
local cntnr ---@type mgmt_container|crdn_container
|
||||
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_packet()
|
||||
cntnr = comms.mgmt_container()
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
pkt = comms.crdn_packet()
|
||||
else
|
||||
return
|
||||
end
|
||||
cntnr = comms.crdn_container()
|
||||
else return end
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
cntnr.make(msg_type, msg)
|
||||
frame.make(self.sv_addr, self.sv_seq_num, protocol, cntnr.raw_packet())
|
||||
|
||||
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
|
||||
tx_nic.transmit(config.SVR_Channel, config.CRD_Channel, frame)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- send an API establish request response
|
||||
---@param packet scada_packet
|
||||
---@param rx_frame scada_frame
|
||||
---@param ack ESTABLISH_ACK
|
||||
---@param data any?
|
||||
local function _send_api_establish_ack(packet, ack, data)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
local function _send_api_establish_ack(rx_frame, ack, data)
|
||||
local tx_frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
mgmt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
||||
tx_frame.make(rx_frame.src_addr(), rx_frame.seq_num() + 1, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
|
||||
self.last_api_est_acks[packet.src_addr()] = ack
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
wl_nic.transmit(config.PKT_Channel, config.CRD_Channel, tx_frame)
|
||||
self.last_api_est_acks[rx_frame.src_addr()] = ack
|
||||
end
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
-- send establish request
|
||||
---@param nic nic nic to transmit on
|
||||
local function _send_establish(nic)
|
||||
local ini_nic = tx_nic
|
||||
tx_nic = nic
|
||||
|
||||
self.sv_r_seq_num = nil
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
|
||||
|
||||
tx_nic = ini_nic
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
@@ -309,19 +251,67 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
--#endregion
|
||||
|
||||
--#region PUBLIC FUNCTIONS --
|
||||
|
||||
---@class coord_comms
|
||||
local public = {}
|
||||
|
||||
-- try to connect to the supervisor if not already linked
|
||||
-- switch the current active NIC
|
||||
---@param new_nic nic
|
||||
function public.switch_nic(new_nic)
|
||||
if tx_nic.is_connected() then
|
||||
-- try to gracefully switch, we have an intact continuous connection
|
||||
log.info(util.c("switching link to reconnected interface ", new_nic.phy_name(), " from ", tx_nic.phy_name()))
|
||||
|
||||
tx_nic = new_nic
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.SWITCH_NET, {})
|
||||
else
|
||||
-- can't gracefully switch, the other NIC was lost
|
||||
log.info(util.c("closing link on ", tx_nic.phy_name(), ", switching to ", new_nic.phy_name()))
|
||||
|
||||
tx_nic = new_nic
|
||||
sv_watchdog.cancel()
|
||||
public.unlink()
|
||||
end
|
||||
end
|
||||
|
||||
-- maintain the supervisor connection, which consists of establishing it and handling link failover
|
||||
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
|
||||
---@return boolean ok, boolean start_ui
|
||||
function public.try_connect(abort)
|
||||
local ok = true
|
||||
local start_ui = false
|
||||
function public.manage_link(abort)
|
||||
local ok, start_ui = true, false
|
||||
|
||||
if self.sv_linked then
|
||||
-- handle connection failover
|
||||
local act_nic = backplane.active_nic()
|
||||
if (act_nic ~= tx_nic) and act_nic.is_network_up() and ((util.time_ms() - self.failover_init) > FAILOVER_GRACE_PERIOD_MS) then
|
||||
log.info(util.c("primary interface ", act_nic.phy_name(), " is up, requesting link switch"))
|
||||
|
||||
tx_nic = act_nic
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.SWITCH_NET, {})
|
||||
|
||||
self.failover_init = util.time_ms()
|
||||
end
|
||||
|
||||
-- handle UI
|
||||
if self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
self.est_tick_waiting = nil
|
||||
self.est_task_done = nil
|
||||
start_ui = true
|
||||
end
|
||||
else
|
||||
local a_nic, s_nic = backplane.active_nic(), backplane.standby_nic()
|
||||
local e_nic = nil
|
||||
|
||||
if a_nic.is_network_up() then
|
||||
e_nic = a_nic
|
||||
elseif s_nic and s_nic.is_network_up() then
|
||||
e_nic = s_nic
|
||||
end
|
||||
|
||||
if not self.sv_linked then
|
||||
if self.est_tick_waiting == nil then
|
||||
self.est_start = os.clock()
|
||||
self.est_last = self.est_start
|
||||
@@ -329,7 +319,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
self.est_tick_waiting, self.est_task_done =
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
|
||||
|
||||
_send_establish()
|
||||
if e_nic then _send_establish(e_nic) end
|
||||
else
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start)))
|
||||
end
|
||||
@@ -359,39 +349,33 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
ok = false
|
||||
elseif (os.clock() - self.est_last) > 1.0 then
|
||||
_send_establish()
|
||||
if e_nic then _send_establish(e_nic) end
|
||||
self.est_last = os.clock()
|
||||
end
|
||||
elseif self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
self.est_tick_waiting = nil
|
||||
self.est_task_done = nil
|
||||
start_ui = true
|
||||
end
|
||||
|
||||
return ok, start_ui
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
-- unlink from the server
|
||||
function public.unlink()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
ioctl.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
|
||||
public.unlink()
|
||||
end
|
||||
|
||||
-- send the resume ready state to the supervisor
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
|
||||
mode, burn_target, charge_target, gen_target, limits
|
||||
})
|
||||
---@param settings auto_ctl_cfg auto control settings
|
||||
function public.send_ready(settings)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, { table.unpack(settings) })
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
@@ -402,15 +386,9 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function public.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, mode, burn_target, charge_target, gen_target, limits
|
||||
})
|
||||
---@param settings auto_ctl_cfg auto control settings
|
||||
function public.send_auto_start(settings)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, table.unpack(settings) })
|
||||
end
|
||||
|
||||
-- send a unit command
|
||||
@@ -427,34 +405,31 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|crdn_frame|nil packet
|
||||
---@return mgmt_packet|crdn_packet|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local pkt, nic = nil, backplane.nics[side]
|
||||
|
||||
if s_pkt then
|
||||
-- get as SCADA management packet
|
||||
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
if nic then
|
||||
local frame = nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if frame then
|
||||
if frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_container().decode(frame)
|
||||
elseif frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
pkt = comms.crdn_container().decode(frame)
|
||||
else
|
||||
log.debug("parse_packet(" .. side .. "): attempted parse of illegal packet type " .. frame.protocol(), true)
|
||||
end
|
||||
-- get as coordinator packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
else
|
||||
log.error("parse_packet(" .. side .. "): received a packet from an interface without a nic?")
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
---@param packet mgmt_packet|crdn_packet|nil
|
||||
---@return boolean close_ui
|
||||
function public.handle_packet(packet)
|
||||
local was_linked = self.sv_linked
|
||||
@@ -468,30 +443,32 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
if l_chan ~= config.CRD_Channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == config.PKT_Channel then
|
||||
if not self.sv_linked then
|
||||
if not config.API_Enabled then
|
||||
-- log.debug("discarding pocket API packet due to the API being disabled")
|
||||
elseif not self.sv_linked then
|
||||
log.debug("discarding pocket API packet before linked to supervisor")
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
---@cast packet crdn_packet
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- coordinator packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
session.in_queue.push_network(packet)
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug("discarding SCADA_CRDN packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
---@cast packet mgmt_packet
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- SCADA management packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
session.in_queue.push_network(packet)
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- establish a new session
|
||||
-- validate packet and continue
|
||||
@@ -518,7 +495,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
local id = apisessions.establish_session(src_addr, packet.scada_frame.seq_num(), firmware_v)
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
local conf = iocontrol.get_db().facility.conf
|
||||
local conf = ioctl.get_db().facility.conf
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling })
|
||||
else
|
||||
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
|
||||
@@ -554,13 +531,13 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
---@cast packet crdn_packet
|
||||
if self.sv_linked then
|
||||
if packet.type == CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.length == 2 then
|
||||
-- record builds
|
||||
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
|
||||
local unit_builds = iocontrol.record_unit_builds(packet.data[2])
|
||||
local fac_builds = ioctl.record_facility_builds(packet.data[1])
|
||||
local unit_builds = ioctl.record_unit_builds(packet.data[2])
|
||||
|
||||
if fac_builds and unit_builds then
|
||||
-- acknowledge receipt of builds
|
||||
@@ -574,7 +551,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
elseif packet.type == CRDN_TYPE.FAC_BUILDS then
|
||||
if packet.length == 1 then
|
||||
-- record facility builds
|
||||
if iocontrol.record_facility_builds(packet.data[1]) then
|
||||
if ioctl.record_facility_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
|
||||
else
|
||||
@@ -585,7 +562,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.FAC_STATUS then
|
||||
-- update facility status
|
||||
if not iocontrol.update_facility_status(packet.data) then
|
||||
if not ioctl.update_facility_status(packet.data) then
|
||||
log.debug("received invalid FAC_STATUS packet")
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.FAC_CMD then
|
||||
@@ -599,7 +576,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
process.fac_ack(cmd, ack)
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if packet.length == 7 then
|
||||
if packet.length == 9 then
|
||||
process.start_ack_handle({ table.unpack(packet.data, 2) })
|
||||
else
|
||||
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
|
||||
@@ -621,7 +598,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
|
||||
-- record builds
|
||||
if packet.length == 1 then
|
||||
if iocontrol.record_unit_builds(packet.data[1]) then
|
||||
if ioctl.record_unit_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
|
||||
else
|
||||
@@ -632,7 +609,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_unit_statuses(packet.data) then
|
||||
if not ioctl.update_unit_statuses(packet.data) then
|
||||
log.debug("received invalid UNIT_STATUSES packet")
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.UNIT_CMD then
|
||||
@@ -642,7 +619,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
local unit_id = packet.data[2]
|
||||
local ack = packet.data[3] == true
|
||||
|
||||
local unit = iocontrol.get_db().units[unit_id]
|
||||
local unit = ioctl.get_db().units[unit_id]
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
@@ -669,7 +646,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
log.debug("discarding SCADA_CRDN packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
---@cast packet mgmt_packet
|
||||
if self.sv_linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
@@ -683,7 +660,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
|
||||
|
||||
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
|
||||
ioctl.get_db().facility.ps.publish("sv_ping", trip_time)
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
@@ -692,10 +669,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
public.unlink()
|
||||
log.info("server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
@@ -708,7 +682,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
-- reset to disconnected before validating
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
ioctl.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
|
||||
if type(sv_config) == "table" and #sv_config == 2 then
|
||||
-- get configuration
|
||||
@@ -720,14 +694,18 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
}
|
||||
|
||||
if conf.num_units == config.UnitCount then
|
||||
tx_nic = backplane.nics[packet.scada_frame.interface()]
|
||||
|
||||
log.info(util.c("supervisor establish request approved, linked to SV (CID#", src_addr, ") on ", tx_nic.phy_name()))
|
||||
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
|
||||
ioctl.init(conf, public, config.TempScale, config.EnergyScale)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
self.sv_config_err = false
|
||||
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
|
||||
ioctl.fp_link_state(types.PANEL_LINK_STATE.LINKED)
|
||||
else
|
||||
self.sv_config_err = true
|
||||
log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
|
||||
@@ -745,17 +723,17 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
|
||||
ioctl.fp_link_state(types.PANEL_LINK_STATE.DENIED)
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
|
||||
ioctl.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
|
||||
log.warning("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
|
||||
ioctl.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
|
||||
log.warning("supervisor comms version mismatch")
|
||||
end
|
||||
else
|
||||
@@ -784,6 +762,8 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
---@nodiscard
|
||||
function public.is_linked() return self.sv_linked end
|
||||
|
||||
--#endregion
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
@@ -31,28 +31,29 @@ local SPS_STATE = types.SPS_STATE
|
||||
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
|
||||
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
|
||||
|
||||
local iocontrol = {}
|
||||
local ioctl = {}
|
||||
|
||||
---@class ioctl
|
||||
local io = {}
|
||||
local _ioctl = {
|
||||
-- connection states for status evaluation
|
||||
wd_modem = true,
|
||||
wl_modem = true,
|
||||
speaker = true,
|
||||
monitor_states = {},
|
||||
coroutines = {}
|
||||
}
|
||||
|
||||
-- initialize front panel PSIL
|
||||
---@param firmware_v string coordinator version
|
||||
---@param comms_v string comms version
|
||||
function iocontrol.init_fp(firmware_v, comms_v)
|
||||
---@class ioctl_front_panel
|
||||
io.fp = { ps = psil.create() }
|
||||
|
||||
io.fp.ps.publish("version", firmware_v)
|
||||
io.fp.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
---@class crd_io
|
||||
local io = {
|
||||
---@class crd_io_fp
|
||||
fp = { ps = psil.create() }
|
||||
}
|
||||
|
||||
-- initialize the coordinator IO controller
|
||||
---@param conf facility_conf configuration
|
||||
---@param comms coord_comms comms reference
|
||||
---@param temp_scale TEMP_SCALE temperature unit
|
||||
---@param energy_scale ENERGY_SCALE energy unit
|
||||
function iocontrol.init(conf, comms, temp_scale, energy_scale)
|
||||
function ioctl.init(conf, comms, temp_scale, energy_scale)
|
||||
io.temp_label = TEMP_UNITS[temp_scale]
|
||||
io.energy_label = ENERGY_UNITS[energy_scale]
|
||||
|
||||
@@ -81,7 +82,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
|
||||
end
|
||||
|
||||
-- facility data structure
|
||||
---@class ioctl_facility
|
||||
---@class crd_io_facility
|
||||
io.facility = {
|
||||
conf = conf,
|
||||
num_units = conf.num_units,
|
||||
@@ -152,12 +153,12 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
|
||||
end
|
||||
|
||||
-- create unit data structures
|
||||
io.units = {} ---@type ioctl_unit[]
|
||||
io.units = {} ---@type crd_io_unit[]
|
||||
for i = 1, conf.num_units do
|
||||
local function ack(alarm) process.ack_alarm(i, alarm) end
|
||||
local function reset(alarm) process.reset_alarm(i, alarm) end
|
||||
|
||||
---@class ioctl_unit
|
||||
---@class crd_io_unit
|
||||
local entry = {
|
||||
unit_id = i,
|
||||
connected = false,
|
||||
@@ -287,24 +288,73 @@ end
|
||||
|
||||
--#region Front Panel PSIL
|
||||
|
||||
-- toggle heartbeat indicator
|
||||
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
|
||||
-- evaluate and publish system health status
|
||||
local function fp_eval_status()
|
||||
local ok = _ioctl.wd_modem and _ioctl.wl_modem and _ioctl.speaker
|
||||
for _, v in pairs(_ioctl.monitor_states) do ok = ok and v end
|
||||
for _, v in pairs(_ioctl.coroutines) do ok = ok and v end
|
||||
|
||||
-- report presence of the wireless modem
|
||||
io.fp.ps.publish("status", ok)
|
||||
end
|
||||
|
||||
-- toggle heartbeat indicator
|
||||
function ioctl.heartbeat() io.fp.ps.toggle("heartbeat") end
|
||||
|
||||
-- report versions to front panel
|
||||
---@param firmware_v string coordinator version
|
||||
---@param comms_v string comms version
|
||||
function ioctl.fp_versions(firmware_v, comms_v)
|
||||
io.fp.ps.publish("version", firmware_v)
|
||||
io.fp.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- report presence of the wired comms modem
|
||||
---@param has_modem boolean
|
||||
function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end
|
||||
function ioctl.fp_has_wd_modem(has_modem)
|
||||
io.fp.ps.publish("has_wd_modem", has_modem)
|
||||
|
||||
_ioctl.wd_modem = has_modem
|
||||
fp_eval_status()
|
||||
end
|
||||
|
||||
-- report presence of the wireless comms modem
|
||||
---@param has_modem boolean
|
||||
function ioctl.fp_has_wl_modem(has_modem)
|
||||
io.fp.ps.publish("has_wl_modem", has_modem)
|
||||
|
||||
_ioctl.wl_modem = has_modem
|
||||
fp_eval_status()
|
||||
end
|
||||
|
||||
-- report if the wired network is up
|
||||
---@param up boolean
|
||||
function ioctl.fp_has_wd_net(up)
|
||||
io.fp.ps.publish("has_wd_net", up)
|
||||
end
|
||||
|
||||
-- report if the wireless network is up
|
||||
---@param up boolean
|
||||
function ioctl.fp_has_wl_net(up)
|
||||
io.fp.ps.publish("has_wl_net", up)
|
||||
end
|
||||
|
||||
-- report presence of the speaker
|
||||
---@param has_speaker boolean
|
||||
function iocontrol.fp_has_speaker(has_speaker) io.fp.ps.publish("has_speaker", has_speaker) end
|
||||
function ioctl.fp_has_speaker(has_speaker)
|
||||
io.fp.ps.publish("has_speaker", has_speaker)
|
||||
|
||||
_ioctl.speaker = has_speaker
|
||||
fp_eval_status()
|
||||
end
|
||||
|
||||
-- report supervisor link state
|
||||
---@param state integer
|
||||
function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) end
|
||||
function ioctl.fp_link_state(state) io.fp.ps.publish("link_state", state) end
|
||||
|
||||
-- report monitor connection state
|
||||
---@param id string|integer unit ID for unit monitor, "main" for main monitor, or "flow" for flow monitor
|
||||
function iocontrol.fp_monitor_state(id, connected)
|
||||
---@param connected 1|2|3 1 for disconnected, 2 for connected but no view (may not fit), 3 for connected with view rendered
|
||||
function ioctl.fp_monitor_state(id, connected)
|
||||
local name = nil
|
||||
|
||||
if id == "main" then
|
||||
@@ -317,21 +367,29 @@ function iocontrol.fp_monitor_state(id, connected)
|
||||
|
||||
if name ~= nil then
|
||||
io.fp.ps.publish(name, connected)
|
||||
|
||||
_ioctl.monitor_states[name] = connected ~= 1
|
||||
fp_eval_status()
|
||||
end
|
||||
end
|
||||
|
||||
-- report thread (routine) statuses
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function iocontrol.fp_rt_status(thread, ok)
|
||||
io.fp.ps.publish(util.c("routine__", thread), ok)
|
||||
function ioctl.fp_rt_status(thread, ok)
|
||||
local name = util.c("routine__", thread)
|
||||
|
||||
io.fp.ps.publish(name, ok)
|
||||
|
||||
_ioctl.coroutines[name] = ok
|
||||
fp_eval_status()
|
||||
end
|
||||
|
||||
-- report PKT firmware version and PKT session connection state
|
||||
---@param session_id integer PKT session
|
||||
---@param fw string firmware version
|
||||
---@param s_addr integer PKT computer ID
|
||||
function iocontrol.fp_pkt_connected(session_id, fw, s_addr)
|
||||
function ioctl.fp_pkt_connected(session_id, fw, s_addr)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_fw", fw)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr))
|
||||
pgi.create_pkt_entry(session_id)
|
||||
@@ -339,14 +397,14 @@ end
|
||||
|
||||
-- report PKT session disconnected
|
||||
---@param session_id integer PKT session
|
||||
function iocontrol.fp_pkt_disconnected(session_id)
|
||||
function ioctl.fp_pkt_disconnected(session_id)
|
||||
pgi.delete_pkt_entry(session_id)
|
||||
end
|
||||
|
||||
-- transmit PKT session RTT
|
||||
---@param session_id integer PKT session
|
||||
---@param rtt integer round trip time
|
||||
function iocontrol.fp_pkt_rtt(session_id, rtt)
|
||||
function ioctl.fp_pkt_rtt(session_id, rtt)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt", rtt)
|
||||
|
||||
if rtt > HIGH_RTT then
|
||||
@@ -392,7 +450,7 @@ end
|
||||
-- populate facility structure builds
|
||||
---@param build table
|
||||
---@return boolean valid
|
||||
function iocontrol.record_facility_builds(build)
|
||||
function ioctl.record_facility_builds(build)
|
||||
local valid = true
|
||||
|
||||
if type(build) == "table" then
|
||||
@@ -435,12 +493,12 @@ end
|
||||
-- populate unit structure builds
|
||||
---@param builds table
|
||||
---@return boolean valid
|
||||
function iocontrol.record_unit_builds(builds)
|
||||
function ioctl.record_unit_builds(builds)
|
||||
local valid = true
|
||||
|
||||
-- note: if not all units and RTUs are connected, some will be nil
|
||||
for id, build in pairs(builds) do
|
||||
local unit = io.units[id] ---@type ioctl_unit
|
||||
local unit = io.units[id] ---@type crd_io_unit
|
||||
|
||||
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
|
||||
|
||||
@@ -566,7 +624,7 @@ end
|
||||
-- update facility status
|
||||
---@param status table
|
||||
---@return boolean valid
|
||||
function iocontrol.update_facility_status(status)
|
||||
function ioctl.update_facility_status(status)
|
||||
local valid = true
|
||||
local log_header = util.c("iocontrol.update_facility_status: ")
|
||||
|
||||
@@ -858,7 +916,7 @@ end
|
||||
-- update unit statuses
|
||||
---@param statuses table
|
||||
---@return boolean valid
|
||||
function iocontrol.update_unit_statuses(statuses)
|
||||
function ioctl.update_unit_statuses(statuses)
|
||||
local valid = true
|
||||
|
||||
if type(statuses) ~= "table" then
|
||||
@@ -1312,6 +1370,6 @@ end
|
||||
--#endregion
|
||||
|
||||
-- get the IO controller database
|
||||
function iocontrol.get_db() return io end
|
||||
function ioctl.get_db() return io end
|
||||
|
||||
return iocontrol
|
||||
return ioctl
|
||||
@@ -19,14 +19,17 @@ local REQUEST_TIMEOUT_MS = 10000
|
||||
local process = {}
|
||||
|
||||
local pctl = {
|
||||
io = nil, ---@type ioctl
|
||||
io = nil, ---@type crd_io
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class sys_control_states
|
||||
control_states = {
|
||||
---@class sys_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE, ---@type PROCESS
|
||||
alt_mode = false,
|
||||
burn_target = 0.0,
|
||||
range_start = 10,
|
||||
range_stop = 90,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {}, ---@type number[]
|
||||
@@ -64,10 +67,10 @@ end
|
||||
--#region Core
|
||||
|
||||
-- initialize the process controller
|
||||
---@param iocontrol ioctl iocontrl system
|
||||
---@param crd_io crd_io iocontrol system
|
||||
---@param coord_comms coord_comms coordinator communications
|
||||
function process.init(iocontrol, coord_comms)
|
||||
pctl.io = iocontrol
|
||||
function process.init(crd_io, coord_comms)
|
||||
pctl.io = crd_io
|
||||
pctl.comms = coord_comms
|
||||
|
||||
-- create command handling objects
|
||||
@@ -85,29 +88,13 @@ function process.init(iocontrol, coord_comms)
|
||||
|
||||
local ctrl_states = settings.get("ControlStates", {}) ---@type sys_control_states
|
||||
local config = ctrl_states.process
|
||||
local f_ps = crd_io.facility.ps
|
||||
|
||||
-- facility auto control configuration
|
||||
if type(config) == "table" then
|
||||
ctl_proc.mode = config.mode
|
||||
ctl_proc.burn_target = config.burn_target
|
||||
ctl_proc.charge_target = config.charge_target
|
||||
ctl_proc.gen_target = config.gen_target
|
||||
ctl_proc.limits = config.limits
|
||||
ctl_proc.waste_product = config.waste_product
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
ctl_proc.sps_low_power = config.sps_low_power
|
||||
|
||||
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
|
||||
pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
pctl.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
|
||||
|
||||
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
|
||||
local unit = pctl.io.units[id]
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
-- update each field if present in the config
|
||||
for key, _ in pairs(ctl_proc) do
|
||||
ctl_proc[key] = config[key] or ctl_proc[key]
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
@@ -118,6 +105,22 @@ function process.init(iocontrol, coord_comms)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power)
|
||||
end
|
||||
|
||||
f_ps.publish("process_mode", ctl_proc.mode)
|
||||
f_ps.publish("process_alt_mode", ctl_proc.alt_mode)
|
||||
f_ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
f_ps.publish("process_range_start", ctl_proc.range_start)
|
||||
f_ps.publish("process_range_stop", ctl_proc.range_stop)
|
||||
f_ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
f_ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
|
||||
f_ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
f_ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
f_ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
|
||||
|
||||
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
|
||||
local unit = pctl.io.units[id]
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
end
|
||||
|
||||
-- unit waste states
|
||||
local waste_modes = ctrl_states.waste_modes
|
||||
if type(waste_modes) == "table" then
|
||||
@@ -142,8 +145,9 @@ function process.init(iocontrol, coord_comms)
|
||||
|
||||
-- report to the supervisor all initial configuration data has been sent
|
||||
-- startup resume can occur if needed
|
||||
local p = ctl_proc
|
||||
pctl.comms.send_ready(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
|
||||
local p = ctl_proc
|
||||
local mode = util.trinary(p.alt_mode and p.mode == PROCESS.CHARGE, PROCESS.RANGE_CONTROL, p.mode)
|
||||
pctl.comms.send_ready({ mode, p.burn_target, p.range_start, p.range_stop, p.charge_target, p.gen_target, p.limits })
|
||||
end
|
||||
|
||||
-- create a handle to process control for usage of commands that get acknowledgements
|
||||
@@ -191,21 +195,19 @@ function process.create_handle()
|
||||
-- start automatic process control with current settings
|
||||
function handle.process_start()
|
||||
if f_request(F_CMD.START, handle.fac_ack.on_start) then
|
||||
local p = pctl.control_states.process
|
||||
pctl.comms.send_auto_start(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
|
||||
local p = pctl.control_states.process
|
||||
local mode = util.trinary(p.alt_mode and p.mode == PROCESS.CHARGE, PROCESS.RANGE_CONTROL, p.mode)
|
||||
|
||||
pctl.comms.send_auto_start({ mode, p.burn_target, p.range_start, p.range_stop, p.charge_target, p.gen_target, p.limits })
|
||||
log.debug("PROCESS: START AUTO CTRL")
|
||||
end
|
||||
end
|
||||
|
||||
-- start automatic process control with remote settings that haven't been set on the coordinator
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function handle.process_start_remote(mode, burn_target, charge_target, gen_target, limits)
|
||||
---@param settings auto_ctl_cfg auto control settings
|
||||
function handle.process_start_remote(settings)
|
||||
if f_request(F_CMD.START, handle.fac_ack.on_start) then
|
||||
pctl.comms.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
|
||||
pctl.comms.send_auto_start(settings)
|
||||
log.debug("PROCESS: START AUTO CTRL")
|
||||
end
|
||||
end
|
||||
@@ -470,20 +472,26 @@ end
|
||||
|
||||
-- save process control settings
|
||||
---@param mode PROCESS process control mode
|
||||
---@param alt_mode boolean true if using range control instead of charge control
|
||||
---@param burn_target number burn rate target
|
||||
---@param range_start integer range control activation threshold
|
||||
---@param range_stop integer range control deactivation threshold
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
function process.save(mode, alt_mode, burn_target, range_start, range_stop, charge_target, gen_target, limits)
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- update config table
|
||||
local ctl_proc = pctl.control_states.process
|
||||
ctl_proc.mode = mode
|
||||
ctl_proc.burn_target = burn_target
|
||||
ctl_proc.charge_target = charge_target
|
||||
ctl_proc.gen_target = gen_target
|
||||
ctl_proc.limits = limits
|
||||
local p = pctl.control_states.process
|
||||
p.mode = mode
|
||||
p.alt_mode = alt_mode
|
||||
p.burn_target = burn_target
|
||||
p.range_start = range_start
|
||||
p.range_stop = range_stop
|
||||
p.charge_target = charge_target
|
||||
p.gen_target = gen_target
|
||||
p.limits = limits
|
||||
|
||||
-- save config
|
||||
pctl.io.facility.save_cfg_ack(_write_auto_config())
|
||||
@@ -494,21 +502,35 @@ end
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
local ctl_proc = pctl.control_states.process
|
||||
ctl_proc.mode = response[2]
|
||||
ctl_proc.burn_target = response[3]
|
||||
ctl_proc.charge_target = response[4]
|
||||
ctl_proc.gen_target = response[5]
|
||||
local p = pctl.control_states.process
|
||||
p.mode = response[2]
|
||||
p.burn_target = response[3]
|
||||
p.range_start = response[4]
|
||||
p.range_stop = response[5]
|
||||
p.charge_target = response[6]
|
||||
p.gen_target = response[7]
|
||||
|
||||
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
for i = 1, math.min(#response[8], pctl.io.facility.num_units) do
|
||||
p.limits[i] = response[8][i]
|
||||
pctl.io.units[i].unit_ps.publish("burn_limit", p.limits[i])
|
||||
end
|
||||
|
||||
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
|
||||
if p.mode == PROCESS.RANGE_CONTROL then
|
||||
p.mode = PROCESS.CHARGE
|
||||
p.alt_mode = true
|
||||
elseif p.mode == PROCESS.CHARGE then
|
||||
p.alt_mode = false
|
||||
end
|
||||
|
||||
local f_ps = pctl.io.facility.ps
|
||||
|
||||
f_ps.publish("process_mode", p.mode)
|
||||
f_ps.publish("process_alt_mode", p.alt_mode)
|
||||
f_ps.publish("process_burn_target", p.burn_target)
|
||||
f_ps.publish("process_range_start", p.range_start)
|
||||
f_ps.publish("process_range_stop", p.range_stop)
|
||||
f_ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(p.charge_target))
|
||||
f_ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(p.gen_target))
|
||||
|
||||
_write_auto_config()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
@@ -28,8 +28,9 @@ local renderer = {}
|
||||
|
||||
-- render engine
|
||||
local engine = {
|
||||
config = nil, ---@type crd_config
|
||||
color_mode = 1, ---@type COLOR_MODE
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
monitors = nil, ---@type crd_displays|nil
|
||||
dmesg_window = nil, ---@type Window|nil
|
||||
ui_ready = false,
|
||||
fp_ready = false,
|
||||
@@ -76,25 +77,18 @@ end
|
||||
-- apply renderer configurations
|
||||
---@param config crd_config
|
||||
function renderer.configure(config)
|
||||
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
engine.config = config
|
||||
engine.color_mode = config.ColorMode
|
||||
engine.disable_flow_view = config.DisableFlowView
|
||||
end
|
||||
|
||||
-- link to the monitor peripherals
|
||||
---@param monitors monitors_struct
|
||||
function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
|
||||
-- report to front panel as connected
|
||||
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
|
||||
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
|
||||
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
|
||||
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
|
||||
end
|
||||
|
||||
-- init all displays in use by the renderer
|
||||
function renderer.init_displays()
|
||||
---@param monitors crd_displays
|
||||
function renderer.init_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
|
||||
-- init main and flow monitors
|
||||
_init_display(engine.monitors.main)
|
||||
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
|
||||
@@ -138,7 +132,7 @@ function renderer.try_start_fp()
|
||||
-- show front panel view on terminal
|
||||
status, msg = pcall(function ()
|
||||
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
panel_view(engine.ui.front_panel, engine.config)
|
||||
end)
|
||||
|
||||
if status then
|
||||
@@ -199,6 +193,7 @@ function renderer.try_start_ui()
|
||||
if engine.monitors.main ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
ioctl.fp_monitor_state("main", 3)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
@@ -206,6 +201,7 @@ function renderer.try_start_ui()
|
||||
if engine.monitors.flow ~= nil then
|
||||
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
ioctl.fp_monitor_state("flow", 3)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
@@ -213,6 +209,7 @@ function renderer.try_start_ui()
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
ioctl.fp_monitor_state(idx, 3)
|
||||
util.nop()
|
||||
end
|
||||
end)
|
||||
@@ -239,9 +236,21 @@ function renderer.close_ui()
|
||||
end
|
||||
|
||||
-- delete element trees
|
||||
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
|
||||
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
|
||||
|
||||
if engine.ui.main_display ~= nil then
|
||||
engine.ui.main_display.delete()
|
||||
ioctl.fp_monitor_state("main", 2)
|
||||
end
|
||||
|
||||
if engine.ui.flow_display ~= nil then
|
||||
engine.ui.flow_display.delete()
|
||||
ioctl.fp_monitor_state("flow", 2)
|
||||
end
|
||||
|
||||
for idx, display in pairs(engine.ui.unit_displays) do
|
||||
display.delete()
|
||||
ioctl.fp_monitor_state(idx, 2)
|
||||
end
|
||||
|
||||
-- report ui as not ready
|
||||
engine.ui_ready = false
|
||||
@@ -275,90 +284,51 @@ function renderer.fp_ready() return engine.fp_ready end
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a monitor peripheral being disconnected
|
||||
---@param device Monitor monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_disconnect(device)
|
||||
local is_used = false
|
||||
|
||||
---@param iface string monitor interface
|
||||
function renderer.handle_disconnect(iface)
|
||||
if not engine.monitors then return false end
|
||||
|
||||
if engine.monitors.main == device then
|
||||
if engine.monitors.main_iface == iface then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
log_render("closed main view due to monitor disconnect")
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.main = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
elseif engine.monitors.flow == device then
|
||||
elseif engine.monitors.flow_iface == iface then
|
||||
if engine.ui.flow_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.flow_display.delete()
|
||||
log_render("closed flow view due to monitor disconnect")
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.flow = nil
|
||||
engine.ui.flow_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
for idx, u_iface in pairs(engine.monitors.unit_ifaces) do
|
||||
if u_iface == iface then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
log_render("closed unit" .. idx .. "view due to monitor disconnect")
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor peripheral being reconnected
|
||||
---@param name string monitor name
|
||||
---@param device Monitor monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_reconnect(name, device)
|
||||
local is_used = false
|
||||
|
||||
if not engine.monitors then return false end
|
||||
|
||||
function renderer.handle_reconnect(name)
|
||||
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
|
||||
-- since it can handle a monitor being reconnected that isn't the right size
|
||||
|
||||
if engine.monitors.main_name == name then
|
||||
is_used = true
|
||||
engine.monitors.main = device
|
||||
|
||||
renderer.handle_resize(name)
|
||||
elseif engine.monitors.flow_name == name then
|
||||
is_used = true
|
||||
engine.monitors.flow = device
|
||||
|
||||
renderer.handle_resize(name)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
renderer.handle_resize(name)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
renderer.handle_resize(name)
|
||||
end
|
||||
|
||||
-- handle a monitor being resized<br>
|
||||
@@ -372,7 +342,7 @@ function renderer.handle_resize(name)
|
||||
|
||||
if not engine.monitors then return false, false end
|
||||
|
||||
if engine.monitors.main_name == name and engine.monitors.main then
|
||||
if engine.monitors.main_iface == name and engine.monitors.main then
|
||||
local device = engine.monitors.main ---@type Monitor
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
@@ -390,7 +360,7 @@ function renderer.handle_resize(name)
|
||||
ui.main_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("main", true)
|
||||
ioctl.fp_monitor_state("main", 2)
|
||||
|
||||
engine.dmesg_window.setVisible(not engine.ui_ready)
|
||||
|
||||
@@ -402,6 +372,8 @@ function renderer.handle_resize(name)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
ioctl.fp_monitor_state("main", 3)
|
||||
|
||||
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.main_display then
|
||||
@@ -411,11 +383,10 @@ function renderer.handle_resize(name)
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
is_ok = false
|
||||
end
|
||||
else engine.dmesg_window.redraw() end
|
||||
elseif engine.monitors.flow_name == name and engine.monitors.flow then
|
||||
elseif engine.monitors.flow_iface == name and engine.monitors.flow then
|
||||
local device = engine.monitors.flow ---@type Monitor
|
||||
|
||||
-- this is necessary if the bottom left block was broken and on reconnect
|
||||
@@ -428,7 +399,7 @@ function renderer.handle_resize(name)
|
||||
ui.flow_display = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
ioctl.fp_monitor_state("flow", 2)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
@@ -438,6 +409,8 @@ function renderer.handle_resize(name)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
ioctl.fp_monitor_state("flow", 3)
|
||||
|
||||
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.flow_display then
|
||||
@@ -447,12 +420,11 @@ function renderer.handle_resize(name)
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
is_ok = false
|
||||
end
|
||||
end
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
for idx, monitor in ipairs(engine.monitors.unit_ifaces) do
|
||||
local device = engine.monitors.unit_displays[idx]
|
||||
|
||||
if monitor == name and device then
|
||||
@@ -466,7 +438,7 @@ function renderer.handle_resize(name)
|
||||
ui.unit_displays[idx] = nil
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
ioctl.fp_monitor_state(idx, 2)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
@@ -476,6 +448,8 @@ function renderer.handle_resize(name)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
ioctl.fp_monitor_state(idx, 3)
|
||||
|
||||
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.unit_displays[idx] then
|
||||
@@ -485,7 +459,6 @@ function renderer.handle_resize(name)
|
||||
|
||||
_print_too_small(device)
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
is_ok = false
|
||||
end
|
||||
end
|
||||
@@ -505,12 +478,12 @@ function renderer.handle_mouse(event)
|
||||
if engine.fp_ready and event.monitor == "terminal" then
|
||||
engine.ui.front_panel.handle_mouse(event)
|
||||
elseif engine.ui_ready then
|
||||
if event.monitor == engine.monitors.main_name then
|
||||
if event.monitor == engine.monitors.main_iface then
|
||||
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
|
||||
elseif event.monitor == engine.monitors.flow_name then
|
||||
elseif event.monitor == engine.monitors.flow_iface then
|
||||
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
for id, monitor in ipairs(engine.monitors.unit_ifaces) do
|
||||
local display = engine.ui.unit_displays[id]
|
||||
if event.monitor == monitor and display then
|
||||
if display then display.handle_mouse(event) end
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
|
||||
local apisessions = {}
|
||||
|
||||
@@ -30,13 +30,9 @@ local function _api_handle_outq(session)
|
||||
local msg = session.out_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- handle a packet to be sent
|
||||
if msg.qtype == mqueue.TYPE.NETWORK then
|
||||
-- handle a SCADA frame to be sent
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- handle instruction/notification
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- instruction/notification with body
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,10 +51,10 @@ local function _shutdown(session)
|
||||
session.open = false
|
||||
session.instance.close()
|
||||
|
||||
-- send packets in out queue (namely the close packet)
|
||||
-- send frames in the out queue (namely the close packet)
|
||||
while session.out_queue.ready() do
|
||||
local msg = session.out_queue.pop()
|
||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||
if msg ~= nil and msg.qtype == mqueue.TYPE.NETWORK then
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
end
|
||||
end
|
||||
@@ -69,7 +65,7 @@ end
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- initialize apisessions
|
||||
---@param nic nic network interface
|
||||
---@param nic nic API network interface
|
||||
---@param config crd_config coordinator config
|
||||
function apisessions.init(nic, config)
|
||||
self.nic = nic
|
||||
@@ -116,7 +112,7 @@ function apisessions.establish_session(source_addr, i_seq_num, version)
|
||||
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
iocontrol.fp_pkt_connected(id, version, source_addr)
|
||||
ioctl.fp_pkt_connected(id, version, source_addr)
|
||||
log.debug(util.c("API: established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
@@ -127,6 +123,7 @@ end
|
||||
|
||||
-- attempt to identify which session's watchdog timer fired
|
||||
---@param timer_event number
|
||||
---@return boolean was_watchdog if this event was one of the watchdogs
|
||||
function apisessions.check_all_watchdogs(timer_event)
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i]
|
||||
@@ -135,9 +132,12 @@ function apisessions.check_all_watchdogs(timer_event)
|
||||
if triggered then
|
||||
log.debug(util.c("API: watchdog closing session ", session, "..."))
|
||||
_shutdown(session)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- iterate all the API sessions
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local pocket = {}
|
||||
|
||||
@@ -16,6 +16,7 @@ local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
local PROCESS = types.PROCESS
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
|
||||
-- retry time constants in ms
|
||||
@@ -79,20 +80,19 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
local function _close()
|
||||
self.conn_watchdog.cancel()
|
||||
self.connected = false
|
||||
iocontrol.fp_pkt_disconnected(id)
|
||||
ioctl.fp_pkt_disconnected(id)
|
||||
end
|
||||
|
||||
-- send a CRDN packet
|
||||
---@param msg_type CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local c_pkt = comms.crdn_packet()
|
||||
local frame, crdn = comms.scada_frame(), comms.crdn_container()
|
||||
|
||||
c_pkt.make(msg_type, msg)
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
crdn.make(msg_type, msg)
|
||||
frame.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, crdn.raw_packet())
|
||||
|
||||
out_queue.push_packet(s_pkt)
|
||||
out_queue.push_network(frame)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -100,13 +100,12 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
out_queue.push_packet(s_pkt)
|
||||
out_queue.push_network(frame)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -120,7 +119,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
f_ack.on_start = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, success }) end
|
||||
f_ack.on_stop = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.STOP, success }) end
|
||||
|
||||
for u = 1, iocontrol.get_db().facility.num_units do
|
||||
for u = 1, ioctl.get_db().facility.num_units do
|
||||
local u_ack = self.proc_handle.unit_ack[u]
|
||||
u_ack.on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
|
||||
u_ack.on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
|
||||
@@ -129,7 +128,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param pkt mgmt_frame|crdn_frame
|
||||
---@param pkt mgmt_packet|crdn_packet
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
@@ -144,9 +143,9 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
---@cast pkt crdn_packet
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == CRDN_TYPE.FAC_CMD then
|
||||
@@ -160,9 +159,9 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
log.info(log_tag .. "STOP PROCESS CTRL")
|
||||
self.proc_handle.process_stop()
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if pkt.length == 6 then
|
||||
if pkt.length == 8 then
|
||||
log.info(log_tag .. "START PROCESS CTRL")
|
||||
self.proc_handle.process_start_remote(pkt.data[2], pkt.data[3], pkt.data[4], pkt.data[5], pkt.data[6])
|
||||
self.proc_handle.process_start_remote({ table.unpack(pkt.data, 2) })
|
||||
else
|
||||
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
|
||||
end
|
||||
@@ -376,13 +375,15 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
}
|
||||
end
|
||||
|
||||
local mode = util.trinary(proc.alt_mode and proc.mode == PROCESS.CHARGE, PROCESS.RANGE_CONTROL, proc.mode)
|
||||
|
||||
-- facility data
|
||||
data[#db.units + 1] = {
|
||||
fac.status_lines,
|
||||
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
|
||||
fac.auto_scram,
|
||||
fac.ascram_status,
|
||||
{ proc.mode, proc.burn_target, proc.charge_target, proc.gen_target }
|
||||
{ mode, proc.burn_target, proc.range_start, proc.range_stop, proc.charge_target, proc.gen_target }
|
||||
}
|
||||
|
||||
_send(CRDN_TYPE.API_GET_PROC, data)
|
||||
@@ -438,7 +439,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@cast pkt mgmt_frame
|
||||
---@cast pkt mgmt_packet
|
||||
if pkt.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
@@ -454,7 +455,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
|
||||
|
||||
iocontrol.fp_pkt_rtt(id, self.last_rtt)
|
||||
ioctl.fp_pkt_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
@@ -510,13 +511,9 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
local message = in_queue.pop()
|
||||
|
||||
if message ~= nil then
|
||||
if message.qtype == mqueue.TYPE.PACKET then
|
||||
if message.qtype == mqueue.TYPE.NETWORK then
|
||||
-- handle a packet
|
||||
_handle_packet(message.message)
|
||||
elseif message.qtype == mqueue.TYPE.COMMAND then
|
||||
-- handle instruction
|
||||
elseif message.qtype == mqueue.TYPE.DATA then
|
||||
-- instruction with body
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,14 +12,15 @@ local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local backplane = require("coordinator.backplane")
|
||||
local configure = require("coordinator.configure")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local COORDINATOR_VERSION = "v1.6.16"
|
||||
local COORDINATOR_VERSION = "v1.8.1"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
@@ -36,45 +37,13 @@ local log_crypto = coordinator.log_crypto
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices (required for monitor setup)
|
||||
ppm.mount_all()
|
||||
|
||||
local wait_on_load = true
|
||||
local loaded, monitors = coordinator.load_config()
|
||||
|
||||
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
|
||||
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
|
||||
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
println("There was a monitor configuration problem at boot.\n")
|
||||
println("Startup will keep trying every 2s in case of chunk load delays.\n")
|
||||
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
|
||||
println("(click to skip to the configurator)")
|
||||
|
||||
local timer_id = util.start_timer(2)
|
||||
|
||||
while true do
|
||||
local event, param1 = util.pull_event()
|
||||
if event == "timer" and param1 == timer_id then
|
||||
-- remount and re-attempt
|
||||
ppm.mount_all()
|
||||
loaded, monitors = coordinator.load_config()
|
||||
break
|
||||
elseif event == "mouse_click" or event == "terminate" then
|
||||
wait_on_load = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if loaded ~= 0 then
|
||||
-- first pass configuration check before validating monitors
|
||||
if not coordinator.load_config() then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(loaded, monitors)
|
||||
local success, error = configure.configure(1)
|
||||
if success then
|
||||
loaded, monitors = coordinator.load_config()
|
||||
if loaded ~= 0 then
|
||||
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
|
||||
if not coordinator.load_config() then
|
||||
println("failed to load a valid configuration, please reconfigure")
|
||||
return
|
||||
end
|
||||
else
|
||||
@@ -83,9 +52,6 @@ if loaded ~= 0 then
|
||||
end
|
||||
end
|
||||
|
||||
-- passed checks, good now
|
||||
---@cast monitors monitors_struct
|
||||
|
||||
local config = coordinator.config
|
||||
|
||||
----------------------------------------
|
||||
@@ -102,6 +68,65 @@ println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
|
||||
crash.set_env("coordinator", COORDINATOR_VERSION)
|
||||
crash.dbg_log_env()
|
||||
|
||||
----------------------------------------
|
||||
-- display init
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices (required for monitor setup)
|
||||
ppm.mount_all()
|
||||
|
||||
local wait_on_load = true
|
||||
|
||||
local disp_ok, disp_err = backplane.init_displays(config)
|
||||
|
||||
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
|
||||
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
|
||||
while wait_on_load and (not disp_ok) and os.clock() < CHUNK_LOAD_DELAY_S do
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
println("There was a monitor configuration problem at boot.\n")
|
||||
println("Startup will keep trying every 2s in case of chunk load delays.\n")
|
||||
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
|
||||
println("(click to skip to the configurator)")
|
||||
|
||||
local timer_id = util.start_timer(2)
|
||||
|
||||
while true do
|
||||
local event, param1 = util.pull_event()
|
||||
if event == "timer" and param1 == timer_id then
|
||||
-- remount and re-attempt
|
||||
ppm.mount_all()
|
||||
disp_ok, disp_err = backplane.init_displays(config)
|
||||
break
|
||||
elseif event == "mouse_click" or event == "terminate" then
|
||||
wait_on_load = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not disp_ok then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(2, disp_err)
|
||||
if success then
|
||||
if not coordinator.load_config() then
|
||||
println("failed to load a valid configuration, please reconfigure")
|
||||
return
|
||||
else
|
||||
disp_ok, disp_err = backplane.init_displays(config)
|
||||
|
||||
if not disp_ok then
|
||||
println(disp_err)
|
||||
println("please reconfigure")
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
println("configuration error: " .. error)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- main application
|
||||
----------------------------------------
|
||||
@@ -111,16 +136,12 @@ local function main()
|
||||
-- system startup
|
||||
----------------------------------------
|
||||
|
||||
-- log mounts now since mounting was done before logging was ready
|
||||
ppm.log_mounts()
|
||||
|
||||
-- report versions/init fp PSIL
|
||||
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
||||
-- report versions
|
||||
ioctl.fp_versions(COORDINATOR_VERSION, comms.version)
|
||||
|
||||
-- init renderer
|
||||
renderer.configure(config)
|
||||
renderer.set_displays(monitors)
|
||||
renderer.init_displays()
|
||||
renderer.init_displays(backplane.displays())
|
||||
renderer.init_dmesg()
|
||||
|
||||
-- lets get started!
|
||||
@@ -130,6 +151,12 @@ local function main()
|
||||
log_sys("system start on " .. os.date("%c"))
|
||||
log_boot("starting " .. COORDINATOR_VERSION)
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
local init_time = network.init_mac(config.AuthKey)
|
||||
log_crypto("HMAC init took " .. init_time .. "ms")
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- memory allocation
|
||||
----------------------------------------
|
||||
@@ -149,15 +176,9 @@ local function main()
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- core coordinator devices
|
||||
crd_dev = {
|
||||
modem = ppm.get_wireless_modem(),
|
||||
speaker = ppm.get_device("speaker") ---@type Speaker|nil
|
||||
},
|
||||
|
||||
-- system objects
|
||||
---@class crd_sys
|
||||
crd_sys = {
|
||||
nic = nil, ---@type nic
|
||||
coord_comms = nil, ---@type coord_comms
|
||||
conn_watchdog = nil ---@type watchdog
|
||||
},
|
||||
@@ -165,68 +186,33 @@ local function main()
|
||||
-- message queues
|
||||
q = {
|
||||
mq_render = mqueue.new()
|
||||
},
|
||||
|
||||
-- message queue message types
|
||||
q_types = {
|
||||
MQ__RENDER_CMD = {
|
||||
START_MAIN_UI = 1,
|
||||
CLOSE_MAIN_UI = 2
|
||||
},
|
||||
MQ__RENDER_DATA = {
|
||||
MON_CONNECT = 1,
|
||||
MON_DISCONNECT = 2,
|
||||
MON_RESIZE = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.crd_dev
|
||||
local smem_sys = __shared_memory.crd_sys
|
||||
|
||||
local smem_sys = __shared_memory.crd_sys
|
||||
local crd_state = __shared_memory.crd_state
|
||||
|
||||
----------------------------------------
|
||||
-- setup alarm sounder subsystem
|
||||
-- init system
|
||||
----------------------------------------
|
||||
|
||||
if smem_dev.speaker == nil then
|
||||
log_boot("annunciator alarm speaker not found")
|
||||
println("startup> speaker not found")
|
||||
log.fatal("no annunciator alarm speaker found")
|
||||
return
|
||||
else
|
||||
local sounder_start = util.time_ms()
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
sounder.init(smem_dev.speaker, config.SpeakerVolume)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
-- modem and speaker initialization
|
||||
if not backplane.init(config, __shared_memory) then return end
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications
|
||||
----------------------------------------
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
local init_time = network.init_mac(config.AuthKey)
|
||||
log_crypto("HMAC init took " .. init_time .. "ms")
|
||||
end
|
||||
|
||||
-- get the communications modem
|
||||
if smem_dev.modem == nil then
|
||||
log_comms("wireless modem not found")
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
return
|
||||
else
|
||||
log_comms("wireless modem connected")
|
||||
iocontrol.fp_has_modem(true)
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
smem_sys.conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- create network interface then setup comms
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
----------------------------------------
|
||||
-- start front panel
|
||||
----------------------------------------
|
||||
|
||||
log_render("starting front panel UI...")
|
||||
|
||||
local fp_message
|
||||
@@ -238,6 +224,16 @@ local function main()
|
||||
return
|
||||
else log_render("front panel ready") end
|
||||
|
||||
-- create connection watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
smem_sys.conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- setup comms
|
||||
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, backplane, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
----------------------------------------
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
@@ -4,8 +4,9 @@ local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local backplane = require("coordinator.backplane")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
local process = require("coordinator.process")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
@@ -20,19 +21,8 @@ local log_comms = coordinator.log_comms
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
local MQ__RENDER_CMD = {
|
||||
START_MAIN_UI = 1,
|
||||
CLOSE_MAIN_UI = 2
|
||||
}
|
||||
|
||||
local MQ__RENDER_DATA = {
|
||||
MON_CONNECT = 1,
|
||||
MON_DISCONNECT = 2,
|
||||
MON_RESIZE = 3
|
||||
}
|
||||
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
|
||||
local RENDER_SLEEP = 100 -- 100ms, 2 ticks
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
@@ -43,8 +33,8 @@ function threads.thread__main(smem)
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("main", true)
|
||||
log.debug("main thread start")
|
||||
ioctl.fp_rt_status("main", true)
|
||||
log.debug("OS: main thread start")
|
||||
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
@@ -54,134 +44,59 @@ function threads.thread__main(smem)
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local nic = smem.crd_sys.nic
|
||||
local coord_comms = smem.crd_sys.coord_comms
|
||||
local conn_watchdog = smem.crd_sys.conn_watchdog
|
||||
local crd_state = smem.crd_state
|
||||
local coord_comms = smem.crd_sys.coord_comms
|
||||
local conn_watchdog = smem.crd_sys.conn_watchdog
|
||||
|
||||
local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD
|
||||
local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA
|
||||
|
||||
-- main loop periodic tasks
|
||||
---@return boolean exit if the application should exit
|
||||
local function loop_tick()
|
||||
-- toggle heartbeat
|
||||
ioctl.heartbeat()
|
||||
|
||||
-- periodic hardware tasks
|
||||
backplane.periodic()
|
||||
|
||||
-- maintain connection
|
||||
local ok, start_ui = coord_comms.manage_link()
|
||||
if not ok then
|
||||
crd_state.link_fail = true
|
||||
crd_state.shutdown = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
return true
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, dispatching main UI start")
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
|
||||
end
|
||||
|
||||
-- iterate sessions and free any closed ones
|
||||
apisessions.iterate_all()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- clear timed out process commands
|
||||
process.clear_timed_out()
|
||||
|
||||
if renderer.ui_ready() then
|
||||
-- update clock used on main and flow monitors
|
||||
ioctl.get_db().facility.ps.publish("date_time", os.date(smem.date_format))
|
||||
end
|
||||
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- event loop
|
||||
while true 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
|
||||
---@cast device Modem
|
||||
-- we only really care if this is our wireless modem
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@cast device Monitor
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
---@cast device Monitor
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_resize" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
crd_state.link_fail = true
|
||||
crd_state.shutdown = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, dispatching main UI start")
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions and free any closed ones
|
||||
apisessions.iterate_all()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- clear timed out process commands
|
||||
process.clear_timed_out()
|
||||
|
||||
if renderer.ui_ready() then
|
||||
-- update clock used on main and flow monitors
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close main UI, connection, and stop sounder
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
coord_comms.close()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
if event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
@@ -194,27 +109,56 @@ function threads.thread__main(smem)
|
||||
coord_comms.close()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "timer" then
|
||||
-- pass this timer event onto the right handler
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
if loop_tick() then break end
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor connection timed out
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close main UI, connection, and stop sounder
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
|
||||
coord_comms.close()
|
||||
sounder.stop()
|
||||
elseif not apisessions.check_all_watchdogs(param1) then -- check API watchdogs
|
||||
-- notify timer callback dispatcher, no other handler claimed this event
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
elseif event == "monitor_resize" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
if type ~= nil and device ~= nil then
|
||||
backplane.attach(type, device, param1)
|
||||
end
|
||||
elseif event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
if type ~= nil and device ~= nil then
|
||||
backplane.detach(type, device, param1)
|
||||
end
|
||||
end
|
||||
|
||||
-- check for termination request or UI crash
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminate requested, main thread exiting")
|
||||
log.info("OS: terminate requested, main thread exiting")
|
||||
elseif not crd_state.ui_ok then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminating due to fatal UI error")
|
||||
log.info("OS: terminating due to fatal UI error")
|
||||
end
|
||||
|
||||
if crd_state.shutdown then
|
||||
-- handle closing supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
coord_comms.manage_link(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("closing supervisor connection...")
|
||||
@@ -242,12 +186,12 @@ function threads.thread__main(smem)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("main", false)
|
||||
ioctl.fp_rt_status("main", false)
|
||||
|
||||
-- if status is true, then we are probably exiting, so this won't matter
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate")
|
||||
if not crd_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
log.info("OS: main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -264,12 +208,15 @@ function threads.thread__render(smem)
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("render", true)
|
||||
log.debug("render thread start")
|
||||
ioctl.fp_rt_status("render", true)
|
||||
log.debug("OS: render thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local render_queue = smem.q.mq_render
|
||||
local crd_state = smem.crd_state
|
||||
local render_queue = smem.q.mq_render
|
||||
|
||||
local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD
|
||||
local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
@@ -317,18 +264,10 @@ function threads.thread__render(smem)
|
||||
|
||||
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
|
||||
-- monitor connected
|
||||
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
|
||||
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
|
||||
end
|
||||
renderer.handle_reconnect(cmd.val)
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
|
||||
-- monitor disconnected
|
||||
if renderer.handle_disconnect(cmd.val) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
renderer.handle_disconnect(cmd.val)
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
|
||||
-- monitor resized
|
||||
local is_used, is_ok = renderer.handle_resize(cmd.val)
|
||||
@@ -336,8 +275,6 @@ function threads.thread__render(smem)
|
||||
log_sys(util.c("configured monitor ", cmd.val, " resized, ", util.trinary(is_ok, "display fits", "display does not fit")))
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
@@ -347,7 +284,7 @@ function threads.thread__render(smem)
|
||||
|
||||
-- check for termination request
|
||||
if crd_state.shutdown then
|
||||
log.info("render thread exiting")
|
||||
log.info("OS: render thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@@ -366,10 +303,10 @@ function threads.thread__render(smem)
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("render", false)
|
||||
ioctl.fp_rt_status("render", false)
|
||||
|
||||
if not crd_state.shutdown then
|
||||
log.info("render thread restarting in 5 seconds...")
|
||||
log.info("OS: render thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -23,7 +23,7 @@ local function new_view(root, x, y, ps)
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -35,7 +35,7 @@ local function new_view(root, x, y, ps, id)
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
@@ -45,10 +45,10 @@ local function new_view(root, x, y, ps, id)
|
||||
-- black has low contrast with dark gray, so if background is black use white instead
|
||||
local cutout_fg_bg = cpair(util.trinary(style.theme.bg == colors.black, colors.white, style.theme.bg), colors.gray)
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,x=1,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,x=1,y=2,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=matrix,text=" ",width=33,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,y=2,fg_bg=cutout_fg_bg}
|
||||
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,y=3}
|
||||
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Pocket Connection Entry
|
||||
--
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -26,7 +26,7 @@ local function init(parent, id)
|
||||
|
||||
local label_fg = style.fp.label_fg
|
||||
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
local ps = ioctl.get_db().fp.ps
|
||||
|
||||
local term_w, _ = term.getSize()
|
||||
|
||||
@@ -36,9 +36,9 @@ local function init(parent, id)
|
||||
|
||||
local ps_prefix = "pkt_" .. id .. "_"
|
||||
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,fg_bg=s_hi_box}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,fg_bg=s_hi_box}
|
||||
TextBox{parent=entry,y=1,text="",width=8,fg_bg=s_hi_box}
|
||||
local pkt_addr = TextBox{parent=entry,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,fg_bg=s_hi_box,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,y=3,text="",width=8,fg_bg=s_hi_box}
|
||||
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
|
||||
|
||||
TextBox{parent=entry,x=10,y=2,text="FW:",width=3}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
@@ -22,6 +22,7 @@ local Checkbox = require("graphics.elements.controls.Checkbox")
|
||||
local HazardButton = require("graphics.elements.controls.HazardButton")
|
||||
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
local SwitchButton = require("graphics.elements.controls.SwitchButton")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -56,14 +57,14 @@ local function new_view(root, x, y)
|
||||
local blk_brn = cpair(colors.black, colors.brown)
|
||||
local blk_pur = cpair(colors.black, colors.purple)
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local facility = db.facility
|
||||
local units = db.units
|
||||
|
||||
local main = Div{parent=root,width=128,height=24,x=x,y=y}
|
||||
|
||||
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
local scram = HazardButton{parent=main,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=db.process.fac_ack_alarms,fg_bg=hzd_fg_bg}
|
||||
|
||||
db.process.fac_ack.on_scram = scram.on_response
|
||||
@@ -125,9 +126,9 @@ local function new_view(root, x, y)
|
||||
-- process control targets --
|
||||
-----------------------------
|
||||
|
||||
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
|
||||
local targets = Div{parent=proc,width=31,height=24,y=1}
|
||||
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
|
||||
local burn_tag = Div{parent=targets,y=1,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
|
||||
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
|
||||
@@ -138,18 +139,47 @@ local function new_view(root, x, y)
|
||||
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
|
||||
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
|
||||
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||
local chg_tag = Div{parent=targets,y=6,width=8,height=4,fg_bg=blk_pur}
|
||||
local chg_tag_text = TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
|
||||
local c_target = NumericSpinbox{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
local range_start, range_stop ---@type NumericSpinbox, NumericSpinbox
|
||||
|
||||
local function _update_start_val(value) range_start.set_value(math.min(range_start.get_value(), value - 1)) end
|
||||
local function _update_stop_val(value) range_stop.set_value(math.max(range_stop.get_value(), value + 1)) end
|
||||
|
||||
local chg_range = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box,hidden=true}
|
||||
TextBox{parent=chg_range,x=2,y=2,text="START",fg_bg=style.theme.label_fg}
|
||||
range_start = NumericSpinbox{parent=chg_range,x=8,y=1,whole_num_precision=3,fractional_precision=0,min=0,max=99,callback=_update_stop_val,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=chg_range,x=11,y=2,text="% \x1a STOP",fg_bg=style.theme.label_fg}
|
||||
range_stop = NumericSpinbox{parent=chg_range,x=20,y=1,whole_num_precision=3,fractional_precision=0,min=1,max=100,callback=_update_start_val,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=chg_range,x=23,y=2,text="%",fg_bg=style.theme.label_fg}
|
||||
|
||||
local cur_charge = DataIndicator{parent=targets,x=11,y=9,label="",format="%17d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
local chg_mode = SwitchButton{parent=targets,x=9,y=9,text="\x12T",active_text="\x12R",callback=function(v)facility.ps.publish("process_alt_mode", v)end,fg_bg=cpair(colors.black,colors.pink),dis_fg_bg=dis_colors}
|
||||
|
||||
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||
range_start.register(facility.ps, "process_range_start", range_start.set_value)
|
||||
range_stop.register(facility.ps, "process_range_stop", range_stop.set_value)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
|
||||
chg_mode.register(facility.ps, "process_alt_mode", chg_mode.set_value)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
targets.register(facility.ps, "process_alt_mode", function (alt)
|
||||
if alt then
|
||||
chg_target.hide()
|
||||
chg_range.show()
|
||||
chg_tag_text.set_value("Charge Range")
|
||||
else
|
||||
chg_target.show()
|
||||
chg_range.hide()
|
||||
chg_tag_text.set_value("Charge Target")
|
||||
end
|
||||
end)
|
||||
|
||||
local gen_tag = Div{parent=targets,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
|
||||
@@ -187,7 +217,7 @@ local function new_view(root, x, y)
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
local unit_tag = Div{parent=limit_div,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
|
||||
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
|
||||
@@ -226,7 +256,7 @@ local function new_view(root, x, y)
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
local unit_tag = Div{parent=stat_div,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
|
||||
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
|
||||
@@ -246,26 +276,48 @@ local function new_view(root, x, y)
|
||||
-------------------------
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
local alt_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Range", "Generation Rate" }
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
local alt_mode = RadioButton{parent=proc,x=34,y=1,options=alt_opts,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple,callback=function(v)mode.set_value(v)end,hidden=true}
|
||||
|
||||
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||
alt_mode.register(facility.ps, "process_mode", alt_mode.set_value)
|
||||
|
||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
|
||||
proc.register(facility.ps, "process_alt_mode", function (alt)
|
||||
if alt then
|
||||
alt_mode.set_value(mode.get_value())
|
||||
mode.hide()
|
||||
alt_mode.show()
|
||||
chg_tag_text.set_value("Charge Range")
|
||||
else
|
||||
mode.set_value(alt_mode.get_value())
|
||||
alt_mode.hide()
|
||||
mode.show()
|
||||
chg_tag_text.set_value("Charge Target")
|
||||
end
|
||||
end)
|
||||
|
||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,y=16,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,y=1,text="UNKNOWN",width=31,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,y=2,text="awaiting data...",width=31,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.white)}
|
||||
|
||||
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
|
||||
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
|
||||
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=s_hi_box}
|
||||
local auto_controls = Div{parent=proc,y=20,width=31,height=5,fg_bg=s_hi_box}
|
||||
|
||||
-- save the automatic process control configuration without starting
|
||||
local function _save_cfg()
|
||||
local limits = {}
|
||||
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
|
||||
|
||||
process.save(mode.get_value(), b_target.get_value(), db.energy_convert_to_fe(c_target.get_value()),
|
||||
db.energy_convert_to_fe(g_target.get_value()), limits)
|
||||
-- make sure stop is always above start (start maxes at 99 and stop maxes at 100 so this always works)
|
||||
if range_stop.get_value() <= range_start.get_value() then
|
||||
range_stop.set_value(range_start.get_value() + 1)
|
||||
end
|
||||
|
||||
process.save(mode.get_value(), chg_mode.get_value(), b_target.get_value(), range_start.get_value(), range_stop.get_value(),
|
||||
db.energy_convert_to_fe(c_target.get_value()), db.energy_convert_to_fe(g_target.get_value()), limits)
|
||||
end
|
||||
|
||||
-- start automatic control after saving process control settings
|
||||
@@ -296,18 +348,26 @@ local function new_view(root, x, y)
|
||||
if active then
|
||||
b_target.disable()
|
||||
c_target.disable()
|
||||
range_start.disable()
|
||||
range_stop.disable()
|
||||
g_target.disable()
|
||||
|
||||
mode.disable()
|
||||
alt_mode.disable()
|
||||
chg_mode.disable()
|
||||
start.disable()
|
||||
|
||||
for i = 1, #rate_limits do rate_limits[i].disable() end
|
||||
else
|
||||
b_target.enable()
|
||||
c_target.enable()
|
||||
range_start.enable()
|
||||
range_stop.enable()
|
||||
g_target.enable()
|
||||
|
||||
mode.enable()
|
||||
alt_mode.enable()
|
||||
chg_mode.enable()
|
||||
if facility.auto_ready then start.enable() end
|
||||
|
||||
for i = 1, #rate_limits do rate_limits[i].enable() end
|
||||
@@ -335,10 +395,10 @@ local function new_view(root, x, y)
|
||||
|
||||
local cutout_fg_bg = cpair(style.theme.bg, colors.brown)
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,x=1,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=waste_sel,text=" ",width=21,y=1,fg_bg=cutout_fg_bg}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,y=2,fg_bg=cutout_fg_bg}
|
||||
|
||||
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
|
||||
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,y=3}
|
||||
local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17}
|
||||
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -25,7 +25,7 @@ local function new_view(root, x, y, ps)
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local reactor = Rectangle{parent=root,border=border(1,colors.gray,true),width=30,height=7,x=x,y=y}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -24,7 +24,7 @@ local function new_view(root, x, y, ps)
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -61,11 +61,11 @@ local function init(parent, id)
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
local unit = db.units[id]
|
||||
local f_ps = db.facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
local main = Div{parent=parent,y=1}
|
||||
|
||||
if unit == nil then return main end
|
||||
|
||||
@@ -146,8 +146,8 @@ local function init(parent, id)
|
||||
-------------------
|
||||
|
||||
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
local stat_line_1 = TextBox{parent=u_stat,y=1,text="UNKNOWN",width=33,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,y=2,text="awaiting data...",width=33,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
|
||||
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
|
||||
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
|
||||
@@ -238,7 +238,7 @@ local function init(parent, id)
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,x=46,y=22}
|
||||
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,y=7}
|
||||
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=ind_bkg,c2=ind_wht.fgd,c3=ind_grn.fgd}
|
||||
@@ -267,7 +267,7 @@ local function init(parent, id)
|
||||
if unit.num_boilers > 0 then
|
||||
if available_space > 0 then _add_space() end
|
||||
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,fg_bg=hc_text}
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,fg_bg=hc_text}
|
||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
|
||||
|
||||
@@ -398,7 +398,7 @@ local function init(parent, id)
|
||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
|
||||
|
||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
|
||||
local waste_mode = MultiButton{parent=waste_div,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
|
||||
|
||||
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
|
||||
|
||||
@@ -484,9 +484,9 @@ local function init(parent, id)
|
||||
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,x=32,y=36}
|
||||
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
|
||||
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
|
||||
local auto_div = Div{parent=auto_ctl,width=13,height=15,y=1}
|
||||
|
||||
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
|
||||
|
||||
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -53,11 +53,11 @@ local function make(parent, x, y, wide, unit_id)
|
||||
|
||||
local height = 16
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local unit = iocontrol.get_db().units[unit_id]
|
||||
local fac = ioctl.get_db().facility
|
||||
local unit = ioctl.get_db().units[unit_id]
|
||||
|
||||
local tank_conns = facility.tank_conns
|
||||
local tank_types = facility.tank_fluid_types
|
||||
local tank_conns = fac.tank_conns
|
||||
local tank_types = fac.tank_fluid_types
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 6)
|
||||
local prv_start = 1 + ((unit.unit_id - 1) * 3)
|
||||
@@ -83,7 +83,7 @@ local function make(parent, x, y, wide, unit_id)
|
||||
-- COOLING LOOP --
|
||||
------------------
|
||||
|
||||
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
local reactor = Rectangle{parent=root,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER}
|
||||
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER}
|
||||
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
@@ -98,7 +98,7 @@ local function make(parent, x, y, wide, unit_id)
|
||||
table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
|
||||
|
||||
if unit.aux_coolant then
|
||||
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
|
||||
local em_water = fac.tank_fluid_types[fac.tank_conns[unit_id]] == COOLANT_TYPE.WATER
|
||||
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
|
||||
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
|
||||
end
|
||||
@@ -191,7 +191,7 @@ local function make(parent, x, y, wide, unit_id)
|
||||
pipe(_wide(132, 110), 6, _wide(130, 108), 6, waste_c, true, true)
|
||||
}
|
||||
|
||||
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=style.theme.bg}
|
||||
PipeNetwork{parent=waste,y=1,pipes=waste_pipes,bg=style.theme.bg}
|
||||
|
||||
local function _valve(vx, vy, n)
|
||||
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2}
|
||||
@@ -207,7 +207,7 @@ local function make(parent, x, y, wide, unit_id)
|
||||
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=style.theme.header,width=l}
|
||||
end
|
||||
|
||||
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local waste_rate = DataIndicator{parent=waste,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=s_field}
|
||||
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=s_field}
|
||||
|
||||
@@ -22,7 +22,7 @@ local pipe = core.pipe
|
||||
---@param parent Container parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param unit ioctl_unit unit database entry
|
||||
---@param unit crd_io_unit unit database entry
|
||||
local function make(parent, x, y, unit)
|
||||
local num_boilers = #unit.boiler_data_tbl
|
||||
local num_turbines = #unit.turbine_data_tbl
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -43,20 +43,20 @@ local function init(main)
|
||||
local lu_col = style.lu_colors
|
||||
local lu_c_d = style.lu_colors_dark
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
local fac = ioctl.get_db().facility
|
||||
local units = ioctl.get_db().units
|
||||
|
||||
local tank_defs = facility.tank_defs
|
||||
local tank_conns = facility.tank_conns
|
||||
local tank_list = facility.tank_list
|
||||
local tank_types = facility.tank_fluid_types
|
||||
local tank_defs = fac.tank_defs
|
||||
local tank_conns = fac.tank_conns
|
||||
local tank_list = fac.tank_list
|
||||
local tank_types = fac.tank_fluid_types
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=style.theme.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
datetime.register(fac.ps, "date_time", datetime.set_value)
|
||||
|
||||
local po_pipes = {}
|
||||
local emcool_pipes = {}
|
||||
@@ -83,9 +83,9 @@ local function init(main)
|
||||
return first, last
|
||||
end
|
||||
|
||||
if facility.tank_mode == 0 or facility.tank_mode == 8 then
|
||||
if fac.tank_mode == 0 or fac.tank_mode == 8 then
|
||||
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
|
||||
for i = 1, facility.num_units do
|
||||
for i = 1, fac.num_units do
|
||||
if units[i].has_tank then
|
||||
local y = y_ofs(i)
|
||||
local color = c_clr(i)
|
||||
@@ -116,7 +116,7 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
|
||||
if facility.tank_mode == 1 then
|
||||
if fac.tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
|
||||
|
||||
@@ -133,7 +133,7 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 2 then
|
||||
elseif fac.tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
|
||||
|
||||
@@ -155,7 +155,7 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 3 then
|
||||
elseif fac.tank_mode == 3 then
|
||||
-- (3) 2 total facility tanks (A A B B)
|
||||
for _, a in pairs({ 1, 3 }) do
|
||||
local b = a + 1
|
||||
@@ -168,7 +168,7 @@ local function init(main)
|
||||
table.insert(emcool_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, c_clr(b), true))
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 4 then
|
||||
elseif fac.tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
|
||||
|
||||
@@ -190,7 +190,7 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 5 then
|
||||
elseif fac.tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
|
||||
|
||||
@@ -212,7 +212,7 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 6 then
|
||||
elseif fac.tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
|
||||
|
||||
@@ -234,7 +234,7 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 7 then
|
||||
elseif fac.tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
|
||||
|
||||
@@ -265,7 +265,7 @@ local function init(main)
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=emcool_pipes,bg=style.theme.bg}
|
||||
end
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
for i = 1, fac.num_units do
|
||||
local y_offset = y_ofs(i)
|
||||
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
|
||||
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
|
||||
@@ -298,7 +298,7 @@ local function init(main)
|
||||
-- auxiliary coolant valves --
|
||||
------------------------------
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
for i = 1, fac.num_units do
|
||||
if units[i].aux_coolant then
|
||||
local vx
|
||||
local vy = 3 + y_ofs(i)
|
||||
@@ -340,7 +340,7 @@ local function init(main)
|
||||
|
||||
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
|
||||
|
||||
TextBox{parent=tank,text=" ",x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=tank,text=" ",y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,fg_bg=style.wh_gray}
|
||||
|
||||
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
|
||||
@@ -376,12 +376,12 @@ local function init(main)
|
||||
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
|
||||
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
|
||||
else
|
||||
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
|
||||
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
|
||||
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
|
||||
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
|
||||
status.register(fac.tank_ps_tbl[f_id], "computed_status", status.update)
|
||||
tank_pcnt.register(fac.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(fac.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(fac.tank_ps_tbl[f_id], "fill", level.update)
|
||||
can_fill.register(fac.tank_ps_tbl[f_id], "container_mode", _can_fill)
|
||||
can_empty.register(fac.tank_ps_tbl[f_id], "container_mode", _can_empty)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -394,24 +394,24 @@ local function init(main)
|
||||
|
||||
local sps = Div{parent=main,x=140,y=3,height=12}
|
||||
|
||||
TextBox{parent=sps,text=" ",width=24,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=sps,text=" ",width=24,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,fg_bg=wh_gray}
|
||||
|
||||
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
|
||||
|
||||
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
|
||||
|
||||
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
|
||||
status.register(fac.sps_ps_tbl[1], "computed_status", status.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",width=10,fg_bg=style.label}
|
||||
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=s_field}
|
||||
|
||||
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
|
||||
sps_in.register(fac.ps, "po_am_rate", sps_in.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",width=15,fg_bg=style.label}
|
||||
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=s_field}
|
||||
|
||||
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
sps_rate.register(fac.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
|
||||
----------------
|
||||
-- statistics --
|
||||
@@ -421,7 +421,7 @@ local function init(main)
|
||||
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
|
||||
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
|
||||
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
|
||||
sum_raw_waste.register(fac.ps, "burn_sum", sum_raw_waste.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
|
||||
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=s_hi_bright}
|
||||
@@ -429,15 +429,15 @@ local function init(main)
|
||||
local po = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="Po",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_c_d,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
|
||||
pu.register(facility.ps, "pu_rate", pu.update)
|
||||
po.register(facility.ps, "po_rate", po.update)
|
||||
popl.register(facility.ps, "po_pl_rate", popl.update)
|
||||
pu.register(fac.ps, "pu_rate", pu.update)
|
||||
po.register(fac.ps, "po_rate", po.update)
|
||||
popl.register(fac.ps, "po_pl_rate", popl.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
|
||||
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=s_hi_bright}
|
||||
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_c_d,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
|
||||
|
||||
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
|
||||
sum_sp_waste.register(fac.ps, "spent_waste_rate", sum_sp_waste.update)
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
@@ -17,6 +17,7 @@ local core = require("graphics.core")
|
||||
local Div = require("graphics.elements.Div")
|
||||
local ListBox = require("graphics.elements.ListBox")
|
||||
local MultiPane = require("graphics.elements.MultiPane")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local TabBar = require("graphics.elements.controls.TabBar")
|
||||
@@ -30,37 +31,79 @@ local LINK_STATE = types.PANEL_LINK_STATE
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local led_grn = style.led_grn
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel DisplayBox main displaybox
|
||||
---@param num_units integer number of units (number of unit monitors)
|
||||
local function init(panel, num_units)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
---@param config crd_config configuration
|
||||
local function init(panel, config)
|
||||
local s_hi_box = style.fp_theme.highlight_box
|
||||
|
||||
local ps = ioctl.get_db().fp.ps
|
||||
|
||||
local term_w, term_h = term.getSize()
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
|
||||
|
||||
local page_div = Div{parent=panel,x=1,y=3}
|
||||
local page_div = Div{parent=panel,y=3}
|
||||
|
||||
--
|
||||
-- system indicators
|
||||
--
|
||||
|
||||
local main_page = Div{parent=page_div,x=1,y=1}
|
||||
local main_page = Div{parent=page_div,y=1}
|
||||
|
||||
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
|
||||
|
||||
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn}
|
||||
status.update(true)
|
||||
system.line_break()
|
||||
|
||||
status.register(ps, "status", status.update)
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=led_grn}
|
||||
if config.WirelessModem and config.WiredModem then
|
||||
local wd_modem = LEDPair{parent=system,label="WD MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
local wl_modem = LEDPair{parent=system,label="WL MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
local function wd_modem_update()
|
||||
if ps.get("has_wd_modem") then
|
||||
if ps.get("has_wd_net") then
|
||||
wd_modem.update(3)
|
||||
else wd_modem.update(2) end
|
||||
else wd_modem.update(1) end
|
||||
end
|
||||
|
||||
local function wl_modem_update()
|
||||
if ps.get("has_wl_modem") then
|
||||
if ps.get("has_wl_net") then
|
||||
wl_modem.update(3)
|
||||
else wl_modem.update(2) end
|
||||
else wl_modem.update(1) end
|
||||
end
|
||||
|
||||
wd_modem.register(ps, "has_wd_modem", wd_modem_update)
|
||||
wd_modem.register(ps, "has_wd_net", wd_modem_update)
|
||||
wl_modem.register(ps, "has_wl_modem", wl_modem_update)
|
||||
wl_modem.register(ps, "has_wl_net", wl_modem_update)
|
||||
else
|
||||
local modem = LEDPair{parent=system,label="MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
local pfx = util.trinary(config.WirelessModem, "has_wl_", "has_wd_")
|
||||
|
||||
local function modem_update()
|
||||
if ps.get(pfx .. "modem") then
|
||||
if ps.get(pfx .. "net") then
|
||||
modem.update(3)
|
||||
else modem.update(2) end
|
||||
else modem.update(1) end
|
||||
end
|
||||
|
||||
modem.register(ps, pfx .. "modem", modem_update)
|
||||
modem.register(ps, pfx .. "net", modem_update)
|
||||
end
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
|
||||
@@ -97,48 +140,44 @@ local function init(panel, num_units)
|
||||
|
||||
system.line_break()
|
||||
|
||||
modem.register(ps, "has_modem", modem.update)
|
||||
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
system.line_break()
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
|
||||
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
|
||||
|
||||
rt_main.register(ps, "routine__main", rt_main.update)
|
||||
rt_render.register(ps, "routine__render", rt_render.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
|
||||
local hmi_devs = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
local speaker = LED{parent=hmi_devs,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
|
||||
main_monitor.register(ps, "main_monitor", main_monitor.update)
|
||||
hmi_devs.line_break()
|
||||
|
||||
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
|
||||
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
|
||||
local main_disp = LEDPair{parent=hmi_devs,label="MAIN DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
main_disp.register(ps, "main_monitor", main_disp.update)
|
||||
|
||||
monitors.line_break()
|
||||
local flow_disp = LEDPair{parent=hmi_devs,label="FLOW DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
flow_disp.register(ps, "flow_monitor", flow_disp.update)
|
||||
|
||||
for i = 1, num_units do
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
|
||||
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
|
||||
hmi_devs.line_break()
|
||||
|
||||
for i = 1, config.UnitCount do
|
||||
local unit_disp = LEDPair{parent=hmi_devs,label="UNIT "..i.." DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
|
||||
unit_disp.register(ps, "unit_monitor_" .. i, unit_disp.update)
|
||||
end
|
||||
|
||||
--
|
||||
-- about footer
|
||||
-- hardware labeling
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
|
||||
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
|
||||
local hw_labels = Rectangle{parent=main_page,x=2,y=term_h-7,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
|
||||
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("%03d", os.getComputerID())
|
||||
|
||||
TextBox{parent=hw_labels,text="FW "..ps.get("version"),fg_bg=s_hi_box}
|
||||
TextBox{parent=hw_labels,text="NT v"..ps.get("comms_version"),fg_bg=s_hi_box}
|
||||
TextBox{parent=hw_labels,text="SN "..comp_id.."-CRD",fg_bg=s_hi_box}
|
||||
|
||||
--
|
||||
-- page handling
|
||||
@@ -146,7 +185,7 @@ local function init(panel, num_units)
|
||||
|
||||
-- API page
|
||||
|
||||
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local api_page = Div{parent=page_div,y=1,hidden=true}
|
||||
local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local _ = Div{parent=api_list,height=1} -- padding
|
||||
|
||||
@@ -154,7 +193,7 @@ local function init(panel, num_units)
|
||||
|
||||
local panes = { main_page, api_page }
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local page_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
|
||||
local tabs = {
|
||||
{ name = "CRD", color = style.fp.text },
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local ioctl = require("coordinator.ioctl")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -25,17 +25,17 @@ local ALIGN = core.ALIGN
|
||||
local function init(main)
|
||||
local s_header = style.theme.header
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
local fac = ioctl.get_db().facility
|
||||
local units = ioctl.get_db().units
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,fg_bg=s_header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
|
||||
local ping = DataIndicator{parent=main,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=s_header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,fg_bg=s_header}
|
||||
|
||||
ping.register(facility.ps, "sv_ping", ping.update)
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
ping.register(fac.ps, "sv_ping", ping.update)
|
||||
datetime.register(fac.ps, "date_time", datetime.set_value)
|
||||
|
||||
---@type Div, Div, Div, Div
|
||||
local uo_1, uo_2, uo_3, uo_4
|
||||
@@ -44,12 +44,12 @@ local function init(main)
|
||||
local row_1_height = 0
|
||||
|
||||
-- unit overviews
|
||||
if facility.num_units >= 1 then
|
||||
if fac.num_units >= 1 then
|
||||
uo_1 = unit_overview(main, 2, 3, units[1])
|
||||
row_1_height = uo_1.get_height()
|
||||
end
|
||||
|
||||
if facility.num_units >= 2 then
|
||||
if fac.num_units >= 2 then
|
||||
uo_2 = unit_overview(main, 84, 3, units[2])
|
||||
row_1_height = math.max(row_1_height, uo_2.get_height())
|
||||
end
|
||||
@@ -58,14 +58,14 @@ local function init(main)
|
||||
|
||||
util.nop()
|
||||
|
||||
if facility.num_units >= 3 then
|
||||
if fac.num_units >= 3 then
|
||||
-- base offset 3, spacing 1, max height of units 1 and 2
|
||||
local row_2_offset = cnc_y_start
|
||||
|
||||
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
|
||||
cnc_y_start = row_2_offset + uo_3.get_height() + 1
|
||||
|
||||
if facility.num_units == 4 then
|
||||
if fac.num_units == 4 then
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
@@ -88,7 +88,7 @@ local function init(main)
|
||||
|
||||
util.nop()
|
||||
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
|
||||
imatrix(main, 131, cnc_bottom_align_start, fac.induction_ps_tbl[1])
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.4.8"
|
||||
core.version = "2.4.10"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
@@ -102,7 +102,7 @@ return function (args)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
|
||||
@@ -13,6 +13,7 @@ local element = require("graphics.element")
|
||||
---@field fractional_precision integer number of fractional digits
|
||||
---@field arrow_fg_bg cpair arrow foreground/background colors
|
||||
---@field arrow_disable? color color when disabled (default light gray)
|
||||
---@field callback? function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -140,6 +141,8 @@ return function (args)
|
||||
|
||||
update_value()
|
||||
show_num()
|
||||
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class switch_button_args
|
||||
---@field text string button text
|
||||
---@field active_text? string button text when active (optional if active_fg_bg set)
|
||||
---@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 active_fg_bg? cpair foreground/background colors when pressed (optional if active_text set)
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -23,8 +27,8 @@ local element = require("graphics.element")
|
||||
return function (args)
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
element.assert((type(args.min_width) == "nil") or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
element.assert((type(args.active_text) == "string") or (type(args.active_fg_bg) == "table"), "active_text or active_fg_bg must be set")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
|
||||
@@ -42,17 +46,22 @@ return function (args)
|
||||
|
||||
-- show the button state
|
||||
function e.redraw()
|
||||
if e.value then
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
if e.enabled then
|
||||
if e.value and args.active_fg_bg then
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
elseif args.dis_fg_bg ~= nil then
|
||||
e.w_set_fgd(args.dis_fg_bg.fgd)
|
||||
e.w_set_bkg(args.dis_fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.clear()
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
e.w_write(util.trinary(e.value and args.active_text, args.active_text, args.text))
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
@@ -68,8 +77,21 @@ return function (args)
|
||||
-- set the value (does not call the callback)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
if e.value ~= val then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- show butten as enabled
|
||||
function e.on_enabled()
|
||||
if args.dis_fg_bg ~= nil then e.redraw() end
|
||||
end
|
||||
|
||||
-- show button as disabled
|
||||
function e.on_disabled()
|
||||
if args.dis_fg_bg ~= nil then e.redraw() end
|
||||
end
|
||||
|
||||
---@class SwitchButton:graphics_element
|
||||
|
||||
@@ -19,6 +19,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field allow_negative? boolean true to allow negative numbers
|
||||
---@field align_right? boolean true to align right while unfocused
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field on_unfocus? function callback when the field becomes unfocused
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -89,6 +90,59 @@ return function (args)
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
|
||||
|
||||
-- parse provided input text and apply it
|
||||
local function _parse_input()
|
||||
local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
|
||||
|
||||
if val then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
|
||||
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||
local parts = util.strtok(str, ".")
|
||||
|
||||
if parts[1] and args.max_int_digits then
|
||||
if string.len(parts[1]) > args.max_int_digits then
|
||||
parts[1] = string.rep("9", args.max_int_digits)
|
||||
ceil = true
|
||||
end
|
||||
end
|
||||
|
||||
if args.allow_decimal and args.max_frac_digits then
|
||||
if ceil then
|
||||
parts[2] = string.rep("9", args.max_frac_digits)
|
||||
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||
-- add a half of the highest precision fractional value in order to round using floor
|
||||
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||
local value = math.floor(scaled + 0.5)
|
||||
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||
end
|
||||
end
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2]) or 0
|
||||
end
|
||||
|
||||
if max and val > max then
|
||||
_set_value(max)
|
||||
ifield.nav_start()
|
||||
elseif min and val < min then
|
||||
_set_value(min)
|
||||
ifield.nav_start()
|
||||
else
|
||||
_set_value(val)
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
e.value = ""
|
||||
end
|
||||
|
||||
ifield.show()
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
@@ -163,14 +217,14 @@ return function (args)
|
||||
---@param min integer minimum allowed value
|
||||
function e.set_min(min)
|
||||
args.min = min
|
||||
e.on_unfocused()
|
||||
_parse_input()
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function e.set_max(max)
|
||||
args.max = max
|
||||
e.on_unfocused()
|
||||
_parse_input()
|
||||
end
|
||||
|
||||
-- replace text with pasted text if its a number
|
||||
@@ -185,55 +239,9 @@ return function (args)
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
|
||||
_parse_input()
|
||||
|
||||
if val then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
|
||||
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||
local parts = util.strtok(str, ".")
|
||||
|
||||
if parts[1] and args.max_int_digits then
|
||||
if string.len(parts[1]) > args.max_int_digits then
|
||||
parts[1] = string.rep("9", args.max_int_digits)
|
||||
ceil = true
|
||||
end
|
||||
end
|
||||
|
||||
if args.allow_decimal and args.max_frac_digits then
|
||||
if ceil then
|
||||
parts[2] = string.rep("9", args.max_frac_digits)
|
||||
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||
-- add a half of the highest precision fractional value in order to round using floor
|
||||
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||
local value = math.floor(scaled + 0.5)
|
||||
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||
end
|
||||
end
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2]) or 0
|
||||
end
|
||||
|
||||
if max and val > max then
|
||||
_set_value(max)
|
||||
ifield.nav_start()
|
||||
elseif min and val < min then
|
||||
_set_value(min)
|
||||
ifield.nav_start()
|
||||
else
|
||||
_set_value(val)
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
e.value = ""
|
||||
end
|
||||
|
||||
ifield.show()
|
||||
if type(args.on_unfocus) == "function" then args.on_unfocus(tonumber(e.value)) end
|
||||
end
|
||||
|
||||
-- handle focus (not unfocus), enable, and redraw with show()
|
||||
|
||||
@@ -11,6 +11,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field max_len? integer maximum string length
|
||||
---@field censor? string character to replace text with when printing to screen
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field on_unfocus? function callback when the field becomes unfocused
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -77,20 +78,20 @@ return function (args)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val string string to set
|
||||
function e.set_value(val)
|
||||
ifield.set_value(val)
|
||||
end
|
||||
e.set_value = ifield.set_value
|
||||
|
||||
-- replace text with pasted text
|
||||
---@param text string string to set
|
||||
function e.handle_paste(text)
|
||||
ifield.set_value(text)
|
||||
e.handle_paste = ifield.set_value
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
ifield.show()
|
||||
|
||||
if type(args.on_unfocus) == "function" then args.on_unfocus(e.value) end
|
||||
end
|
||||
|
||||
-- handle focus, enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_unfocused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
-- Indicator Light Flasher
|
||||
--
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local flasher = {}
|
||||
|
||||
@@ -61,7 +62,7 @@ end
|
||||
---@param f function function to call each period
|
||||
---@param period PERIOD time period option (1, 2, or 3)
|
||||
function flasher.start(f, period)
|
||||
if type(registry[period]) == "table" then
|
||||
if type(registry[period]) == "table" and not util.table_contains(registry[period], f) then
|
||||
table.insert(registry[period], f)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,15 +55,15 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
|
||||
local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24}
|
||||
|
||||
local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}}
|
||||
local ui_pane = MultiPane{parent=net_cfg,y=4,panes={ui_c_1,ui_c_2}}
|
||||
|
||||
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
|
||||
TextBox{parent=ui_cfg,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
|
||||
|
||||
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
|
||||
TextBox{parent=ui_c_1,y=1,height=3,text="You may customize UI options below."}
|
||||
|
||||
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
|
||||
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
@@ -72,16 +72,16 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
ui_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=ui_c_1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
|
||||
TextBox{parent=ui_c_2,y=1,height=3,text="You may customize units below."}
|
||||
|
||||
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
|
||||
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
TextBox{parent=ui_c_2,y=4,text="Temperature Scale"}
|
||||
local temp_scale = RadioButton{parent=ui_c_2,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
|
||||
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
TextBox{parent=ui_c_2,y=10,text="Energy Scale"}
|
||||
local energy_scale = RadioButton{parent=ui_c_2,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
|
||||
|
||||
local function submit_ui_units()
|
||||
tmp_cfg.TempScale = temp_scale.get_value()
|
||||
@@ -89,7 +89,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=ui_c_2,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -101,26 +101,26 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
local net_pane = MultiPane{parent=net_cfg,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
TextBox{parent=net_cfg,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,text="Set network channels."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,y=1,text="Set network channels."}
|
||||
TextBox{parent=net_c_1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,y=10,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,y=12,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=1,y=14,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local chan_err = TextBox{parent=net_c_1,y=14,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||
@@ -131,18 +131,18 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
else chan_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,text="Set connection timeout."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,y=1,text="Set connection timeout."}
|
||||
TextBox{parent=net_c_2,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=11,width=19,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,y=11,width=19,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=1,y=14,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local ct_err = TextBox{parent=net_c_2,y=14,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
@@ -153,16 +153,16 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,text="Set the trusted range."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=1,text="Set the trusted range."}
|
||||
TextBox{parent=net_c_3,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
local range = NumberField{parent=net_c_3,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
local tr_err = TextBox{parent=net_c_3,x=1,y=14,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local tr_err = TextBox{parent=net_c_3,y=14,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_tr()
|
||||
local range_val = tonumber(range.get_value())
|
||||
@@ -173,26 +173,26 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
else tr_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=12,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=12,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_4,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
|
||||
|
||||
-- declare back first so tabbing makes sense visually
|
||||
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local hide_key = Checkbox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_4,x=1,y=14,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local key_err = TextBox{parent=net_c_4,y=14,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
@@ -211,20 +211,20 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
TextBox{parent=log_cfg,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
|
||||
TextBox{parent=log_c_1,y=1,text="Configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
TextBox{parent=log_c_1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=log_c_1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
local en_dbg = Checkbox{parent=log_c_1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=1,y=14,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local path_err = TextBox{parent=log_c_1,y=14,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
@@ -240,7 +240,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,y=15,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -252,11 +252,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
local sum_pane = MultiPane{parent=summary,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
TextBox{parent=summary,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local setting_list = ListBox{parent=sum_c_1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_summary()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
@@ -311,11 +311,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=sum_c_1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,y=13,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_2,y=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
@@ -323,21 +323,21 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
TextBox{parent=sum_c_3,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/pocket/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=sum_c_4,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
@@ -417,7 +417,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
textbox = TextBox{parent=line,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
@@ -127,16 +127,16 @@ local function config_view(display)
|
||||
|
||||
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
local root_pane_div = Div{parent=display,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local ui_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
local main_page = Div{parent=root_pane_div,y=1}
|
||||
local ui_cfg = Div{parent=root_pane_div,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,y=1}
|
||||
local summary = Div{parent=root_pane_div,y=1}
|
||||
local changelog = Div{parent=root_pane_div,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
|
||||
|
||||
--#region Main Page
|
||||
|
||||
@@ -192,20 +192,20 @@ local function config_view(display)
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=changelog,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local c_log = ListBox{parent=cl,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,21)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=cl,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ local TEMP_UNITS = types.TEMP_SCALE_UNITS
|
||||
local WARN_TT = 40
|
||||
local HIGH_TT = 80
|
||||
|
||||
local iocontrol = {}
|
||||
local ioctl = {}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
@@ -30,9 +30,9 @@ local LINK_STATE = {
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
iocontrol.LINK_STATE = LINK_STATE
|
||||
ioctl.LINK_STATE = LINK_STATE
|
||||
|
||||
---@class pocket_ioctl
|
||||
---@class pkt_io
|
||||
local io = {
|
||||
version = "unknown", -- pocket version
|
||||
ps = psil.create(), -- pocket PSIL
|
||||
@@ -46,11 +46,11 @@ local comms = nil ---@type pocket_comms
|
||||
---@param pkt_comms pocket_comms
|
||||
---@param nav pocket_nav
|
||||
---@param cfg pkt_config
|
||||
function iocontrol.init_core(pkt_comms, nav, cfg)
|
||||
function ioctl.init_core(pkt_comms, nav, cfg)
|
||||
comms = pkt_comms
|
||||
config = cfg
|
||||
|
||||
iocontrol.rx = iorx(io)
|
||||
ioctl.rx = iorx(io)
|
||||
|
||||
io.nav = nav
|
||||
|
||||
@@ -106,7 +106,7 @@ end
|
||||
|
||||
-- initialize facility-dependent components of pocket iocontrol
|
||||
---@param conf facility_conf facility configuration
|
||||
function iocontrol.init_fac(conf)
|
||||
function ioctl.init_fac(conf)
|
||||
local temp_scale, energy_scale = config.TempScale, config.EnergyScale
|
||||
io.temp_label = TEMP_UNITS[temp_scale]
|
||||
io.energy_label = ENERGY_UNITS[energy_scale]
|
||||
@@ -136,7 +136,7 @@ function iocontrol.init_fac(conf)
|
||||
end
|
||||
|
||||
-- facility data structure
|
||||
---@class pioctl_facility
|
||||
---@class pkt_io_facility
|
||||
io.facility = {
|
||||
num_units = conf.num_units,
|
||||
tank_mode = conf.cooling.fac_tank_mode,
|
||||
@@ -206,9 +206,9 @@ function iocontrol.init_fac(conf)
|
||||
end
|
||||
|
||||
-- create unit data structures
|
||||
io.units = {} ---@type pioctl_unit[]
|
||||
io.units = {} ---@type pkt_io_unit[]
|
||||
for i = 1, conf.num_units do
|
||||
---@class pioctl_unit
|
||||
---@class pkt_io_unit
|
||||
local entry = {
|
||||
unit_id = i,
|
||||
connected = false,
|
||||
@@ -310,7 +310,7 @@ end
|
||||
---@param state POCKET_LINK_STATE
|
||||
---@param sv_addr integer|false|nil supervisor address if linked, nil if unchanged, false if unlinked
|
||||
---@param api_addr integer|false|nil coordinator address if linked, nil if unchanged, false if unlinked
|
||||
function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
function ioctl.report_link_state(state, sv_addr, api_addr)
|
||||
io.ps.publish("link_state", state)
|
||||
|
||||
if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
||||
@@ -335,14 +335,14 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
end
|
||||
|
||||
-- show the reason the supervisor connection isn't linking
|
||||
function iocontrol.report_svr_link_error(msg) io.ps.publish("svr_link_msg", msg) end
|
||||
function ioctl.report_svr_link_error(msg) io.ps.publish("svr_link_msg", msg) end
|
||||
|
||||
-- show the reason the coordinator api connection isn't linking
|
||||
function iocontrol.report_crd_link_error(msg) io.ps.publish("api_link_msg", msg) end
|
||||
function ioctl.report_crd_link_error(msg) io.ps.publish("api_link_msg", msg) end
|
||||
|
||||
-- determine supervisor connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_svr_tt(trip_time)
|
||||
function ioctl.report_svr_tt(trip_time)
|
||||
local state = 3
|
||||
if trip_time > HIGH_TT then
|
||||
state = 1
|
||||
@@ -355,7 +355,7 @@ end
|
||||
|
||||
-- determine coordinator connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_crd_tt(trip_time)
|
||||
function ioctl.report_crd_tt(trip_time)
|
||||
local state = 3
|
||||
if trip_time > HIGH_TT then
|
||||
state = 1
|
||||
@@ -367,6 +367,6 @@ function iocontrol.report_crd_tt(trip_time)
|
||||
end
|
||||
|
||||
-- get the IO controller database
|
||||
function iocontrol.get_db() return io end
|
||||
function ioctl.get_db() return io end
|
||||
|
||||
return iocontrol
|
||||
return ioctl
|
||||
190
pocket/iorx.lua
190
pocket/iorx.lua
@@ -18,7 +18,7 @@ local TNK_STATE = types.TANK_STATE
|
||||
local MTX_STATE = types.IMATRIX_STATE
|
||||
local SPS_STATE = types.SPS_STATE
|
||||
|
||||
local io ---@type pocket_ioctl
|
||||
local io ---@type pkt_io
|
||||
local iorx = {} ---@class iorx
|
||||
|
||||
-- populate facility data from API_GET_FAC
|
||||
@@ -72,6 +72,7 @@ end
|
||||
---@param data table
|
||||
function iorx.record_unit_data(data)
|
||||
local unit = io.units[data[1]]
|
||||
local u_ps = unit.unit_ps
|
||||
|
||||
unit.connected = data[2]
|
||||
local comp_statuses = data[3]
|
||||
@@ -80,8 +81,8 @@ function iorx.record_unit_data(data)
|
||||
|
||||
local next_c_stat = 1
|
||||
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
u_ps.publish("auto_group_id", unit.a_group)
|
||||
u_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
|
||||
--#region Annunciator
|
||||
|
||||
@@ -106,7 +107,7 @@ function iorx.record_unit_data(data)
|
||||
|
||||
if not every then rcs_disconn = true end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, every)
|
||||
u_ps.publish("U_" .. key, every)
|
||||
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
|
||||
-- split up array for all boilers
|
||||
local any = false
|
||||
@@ -121,7 +122,7 @@ function iorx.record_unit_data(data)
|
||||
rcs_hazard = true
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, any)
|
||||
u_ps.publish("U_" .. key, any)
|
||||
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
|
||||
-- split up array for all turbines
|
||||
local any = false
|
||||
@@ -136,10 +137,10 @@ function iorx.record_unit_data(data)
|
||||
rcs_hazard = true
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_" .. key, any)
|
||||
u_ps.publish("U_" .. key, any)
|
||||
else
|
||||
-- non-table fields
|
||||
unit.unit_ps.publish(key, val)
|
||||
u_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -157,7 +158,7 @@ function iorx.record_unit_data(data)
|
||||
rcs_status = 1
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_RCS", rcs_status)
|
||||
u_ps.publish("U_RCS", rcs_status)
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -193,27 +194,27 @@ function iorx.record_unit_data(data)
|
||||
|
||||
for key, val in pairs(unit.reactor_data) do
|
||||
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
|
||||
unit.unit_ps.publish(key, val)
|
||||
u_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.rps_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
u_ps.publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.mek_struct) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
u_ps.publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.reactor_data.mek_status) do
|
||||
unit.unit_ps.publish(key, val)
|
||||
u_ps.publish(key, val)
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
|
||||
unit.unit_ps.publish("U_ReactorStateStatus", comp_statuses[next_c_stat])
|
||||
unit.unit_ps.publish("U_RPS", rps_status)
|
||||
u_ps.publish("U_ControlStatus", control_status)
|
||||
u_ps.publish("U_ReactorStatus", reactor_status)
|
||||
u_ps.publish("U_ReactorStateStatus", comp_statuses[next_c_stat])
|
||||
u_ps.publish("U_RPS", rps_status)
|
||||
|
||||
next_c_stat = next_c_stat + 1
|
||||
|
||||
@@ -493,7 +494,7 @@ function iorx.record_unit_data(data)
|
||||
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
|
||||
u_ps.publish("U_ECAM", textutils.serialize(ecam))
|
||||
|
||||
--#endregion
|
||||
end
|
||||
@@ -503,43 +504,45 @@ end
|
||||
function iorx.record_control_data(data)
|
||||
for u_id = 1, #data do
|
||||
local unit = io.units[u_id]
|
||||
local u_ps = unit.unit_ps
|
||||
local u_data = data[u_id]
|
||||
local rct = unit.reactor_data
|
||||
|
||||
unit.connected = u_data[1]
|
||||
|
||||
unit.reactor_data.rps_tripped = u_data[2]
|
||||
unit.unit_ps.publish("rps_tripped", u_data[2])
|
||||
unit.reactor_data.mek_status.status = u_data[3]
|
||||
unit.unit_ps.publish("status", u_data[3])
|
||||
unit.reactor_data.mek_status.temp = u_data[4]
|
||||
unit.unit_ps.publish("temp", u_data[4])
|
||||
unit.reactor_data.mek_status.burn_rate = u_data[5]
|
||||
unit.unit_ps.publish("burn_rate", u_data[5])
|
||||
unit.reactor_data.mek_status.act_burn_rate = u_data[6]
|
||||
unit.unit_ps.publish("act_burn_rate", u_data[6])
|
||||
unit.reactor_data.mek_struct.max_burn = u_data[7]
|
||||
unit.unit_ps.publish("max_burn", u_data[7])
|
||||
rct.rps_tripped = u_data[2]
|
||||
u_ps.publish("rps_tripped", u_data[2])
|
||||
rct.mek_status.status = u_data[3]
|
||||
u_ps.publish("status", u_data[3])
|
||||
rct.mek_status.temp = u_data[4]
|
||||
u_ps.publish("temp", u_data[4])
|
||||
rct.mek_status.burn_rate = u_data[5]
|
||||
u_ps.publish("burn_rate", u_data[5])
|
||||
rct.mek_status.act_burn_rate = u_data[6]
|
||||
u_ps.publish("act_burn_rate", u_data[6])
|
||||
rct.mek_struct.max_burn = u_data[7]
|
||||
u_ps.publish("max_burn", u_data[7])
|
||||
|
||||
unit.annunciator.AutoControl = u_data[8]
|
||||
unit.unit_ps.publish("AutoControl", u_data[8])
|
||||
u_ps.publish("AutoControl", u_data[8])
|
||||
|
||||
unit.a_group = u_data[9]
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
u_ps.publish("auto_group_id", unit.a_group)
|
||||
u_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
|
||||
local control_status = 1
|
||||
|
||||
if unit.connected then
|
||||
if unit.reactor_data.rps_tripped then
|
||||
if rct.rps_tripped then
|
||||
control_status = 2
|
||||
end
|
||||
|
||||
if unit.reactor_data.mek_status.status then
|
||||
if rct.mek_status.status then
|
||||
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
|
||||
end
|
||||
end
|
||||
|
||||
unit.unit_ps.publish("U_ControlStatus", control_status)
|
||||
u_ps.publish("U_ControlStatus", control_status)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -549,6 +552,7 @@ function iorx.record_process_data(data)
|
||||
-- get unit data
|
||||
for u_id = 1, #io.units do
|
||||
local unit = io.units[u_id]
|
||||
local u_ps = unit.unit_ps
|
||||
local u_data = data[u_id]
|
||||
|
||||
unit.reactor_data.mek_status.status = u_data[1]
|
||||
@@ -556,18 +560,19 @@ function iorx.record_process_data(data)
|
||||
unit.annunciator.AutoControl = u_data[6]
|
||||
unit.a_group = u_data[7]
|
||||
|
||||
unit.unit_ps.publish("status", u_data[1])
|
||||
unit.unit_ps.publish("max_burn", u_data[2])
|
||||
unit.unit_ps.publish("burn_limit", u_data[3])
|
||||
unit.unit_ps.publish("U_AutoReady", u_data[4])
|
||||
unit.unit_ps.publish("U_AutoDegraded", u_data[5])
|
||||
unit.unit_ps.publish("AutoControl", u_data[6])
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
u_ps.publish("status", u_data[1])
|
||||
u_ps.publish("max_burn", u_data[2])
|
||||
u_ps.publish("burn_limit", u_data[3])
|
||||
u_ps.publish("U_AutoReady", u_data[4])
|
||||
u_ps.publish("U_AutoDegraded", u_data[5])
|
||||
u_ps.publish("AutoControl", u_data[6])
|
||||
u_ps.publish("auto_group_id", unit.a_group)
|
||||
u_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
end
|
||||
|
||||
-- get facility data
|
||||
local fac = io.facility
|
||||
local f_ps = fac.ps
|
||||
local f_data = data[#io.units + 1]
|
||||
|
||||
fac.status_lines = f_data[1]
|
||||
@@ -580,25 +585,27 @@ function iorx.record_process_data(data)
|
||||
fac.auto_scram = f_data[3]
|
||||
fac.ascram_status = f_data[4]
|
||||
|
||||
fac.ps.publish("status_line_1", fac.status_lines[1])
|
||||
fac.ps.publish("status_line_2", fac.status_lines[2])
|
||||
f_ps.publish("status_line_1", fac.status_lines[1])
|
||||
f_ps.publish("status_line_2", fac.status_lines[2])
|
||||
|
||||
fac.ps.publish("auto_ready", fac.auto_ready)
|
||||
fac.ps.publish("auto_active", fac.auto_active)
|
||||
fac.ps.publish("auto_ramping", fac.auto_ramping)
|
||||
fac.ps.publish("auto_saturated", fac.auto_saturated)
|
||||
f_ps.publish("auto_ready", fac.auto_ready)
|
||||
f_ps.publish("auto_active", fac.auto_active)
|
||||
f_ps.publish("auto_ramping", fac.auto_ramping)
|
||||
f_ps.publish("auto_saturated", fac.auto_saturated)
|
||||
|
||||
fac.ps.publish("auto_scram", fac.auto_scram)
|
||||
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
|
||||
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
|
||||
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
|
||||
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
|
||||
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
|
||||
f_ps.publish("auto_scram", fac.auto_scram)
|
||||
f_ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
|
||||
f_ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
|
||||
f_ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
|
||||
f_ps.publish("as_radiation", fac.ascram_status.radiation)
|
||||
f_ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
|
||||
|
||||
fac.ps.publish("process_mode", f_data[5][1])
|
||||
fac.ps.publish("process_burn_target", f_data[5][2])
|
||||
fac.ps.publish("process_charge_target", f_data[5][3])
|
||||
fac.ps.publish("process_gen_target", f_data[5][4])
|
||||
f_ps.publish("process_mode", f_data[5][1])
|
||||
f_ps.publish("process_burn_target", f_data[5][2])
|
||||
f_ps.publish("process_range_start", f_data[5][3])
|
||||
f_ps.publish("process_range_stop", f_data[5][4])
|
||||
f_ps.publish("process_charge_target", f_data[5][5])
|
||||
f_ps.publish("process_gen_target", f_data[5][6])
|
||||
end
|
||||
|
||||
-- update waste app with unit data from API_GET_WASTE
|
||||
@@ -607,6 +614,7 @@ function iorx.record_waste_data(data)
|
||||
-- get unit data
|
||||
for u_id = 1, #io.units do
|
||||
local unit = io.units[u_id]
|
||||
local u_ps = unit.unit_ps
|
||||
local u_data = data[u_id]
|
||||
|
||||
unit.waste_mode = u_data[1]
|
||||
@@ -617,53 +625,55 @@ function iorx.record_waste_data(data)
|
||||
unit.sna_out_rate = u_data[6]
|
||||
unit.waste_stats = u_data[7]
|
||||
|
||||
unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
|
||||
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
|
||||
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
|
||||
u_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
|
||||
u_ps.publish("U_WasteMode", unit.waste_mode)
|
||||
u_ps.publish("U_WasteProduct", unit.waste_product)
|
||||
|
||||
unit.unit_ps.publish("sna_count", unit.num_snas)
|
||||
unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate)
|
||||
unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate)
|
||||
unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate)
|
||||
u_ps.publish("sna_count", unit.num_snas)
|
||||
u_ps.publish("sna_peak_rate", unit.sna_peak_rate)
|
||||
u_ps.publish("sna_max_rate", unit.sna_max_rate)
|
||||
u_ps.publish("sna_out_rate", unit.sna_out_rate)
|
||||
|
||||
unit.unit_ps.publish("pu_rate", unit.waste_stats[1])
|
||||
unit.unit_ps.publish("po_rate", unit.waste_stats[2])
|
||||
unit.unit_ps.publish("po_pl_rate", unit.waste_stats[3])
|
||||
u_ps.publish("pu_rate", unit.waste_stats[1])
|
||||
u_ps.publish("po_rate", unit.waste_stats[2])
|
||||
u_ps.publish("po_pl_rate", unit.waste_stats[3])
|
||||
end
|
||||
|
||||
-- get facility data
|
||||
local fac = io.facility
|
||||
local f_ps = fac.ps
|
||||
local f_data = data[#io.units + 1]
|
||||
|
||||
fac.auto_current_waste_product = f_data[1]
|
||||
fac.auto_pu_fallback_active = f_data[2]
|
||||
fac.auto_sps_disabled = f_data[3]
|
||||
|
||||
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
|
||||
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
|
||||
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
|
||||
f_ps.publish("current_waste_product", fac.auto_current_waste_product)
|
||||
f_ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
|
||||
f_ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
|
||||
|
||||
fac.ps.publish("process_waste_product", f_data[4])
|
||||
fac.ps.publish("process_pu_fallback", f_data[5])
|
||||
fac.ps.publish("process_sps_low_power", f_data[6])
|
||||
f_ps.publish("process_waste_product", f_data[4])
|
||||
f_ps.publish("process_pu_fallback", f_data[5])
|
||||
f_ps.publish("process_sps_low_power", f_data[6])
|
||||
|
||||
fac.waste_stats = f_data[7]
|
||||
|
||||
fac.ps.publish("burn_sum", fac.waste_stats[1])
|
||||
fac.ps.publish("pu_rate", fac.waste_stats[2])
|
||||
fac.ps.publish("po_rate", fac.waste_stats[3])
|
||||
fac.ps.publish("po_pl_rate", fac.waste_stats[4])
|
||||
fac.ps.publish("po_am_rate", fac.waste_stats[5])
|
||||
fac.ps.publish("spent_waste_rate", fac.waste_stats[6])
|
||||
f_ps.publish("burn_sum", fac.waste_stats[1])
|
||||
f_ps.publish("pu_rate", fac.waste_stats[2])
|
||||
f_ps.publish("po_rate", fac.waste_stats[3])
|
||||
f_ps.publish("po_pl_rate", fac.waste_stats[4])
|
||||
f_ps.publish("po_am_rate", fac.waste_stats[5])
|
||||
f_ps.publish("spent_waste_rate", fac.waste_stats[6])
|
||||
|
||||
fac.sps_ps_tbl[1].publish("SPSStateStatus", f_data[8])
|
||||
fac.ps.publish("sps_process_rate", f_data[9])
|
||||
f_ps.publish("sps_process_rate", f_data[9])
|
||||
end
|
||||
|
||||
-- update facility app with facility and unit data from API_GET_FAC_DTL
|
||||
---@param data table
|
||||
function iorx.record_fac_detail_data(data)
|
||||
local fac = io.facility
|
||||
local f_ps = fac.ps
|
||||
|
||||
local tank_statuses = data[5]
|
||||
local next_t_stat = 1
|
||||
@@ -675,14 +685,14 @@ function iorx.record_fac_detail_data(data)
|
||||
fac.auto_scram = data[3]
|
||||
fac.ascram_status = data[4]
|
||||
|
||||
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
|
||||
fac.ps.publish("rtu_count", fac.rtu_count)
|
||||
fac.ps.publish("auto_scram", fac.auto_scram)
|
||||
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
|
||||
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
|
||||
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
|
||||
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
|
||||
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
|
||||
f_ps.publish("all_sys_ok", fac.all_sys_ok)
|
||||
f_ps.publish("rtu_count", fac.rtu_count)
|
||||
f_ps.publish("auto_scram", fac.auto_scram)
|
||||
f_ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
|
||||
f_ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
|
||||
f_ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
|
||||
f_ps.publish("as_radiation", fac.ascram_status.radiation)
|
||||
f_ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
|
||||
|
||||
-- unit data
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
@@ -12,7 +12,7 @@ local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
local LINK_STATE = ioctl.LINK_STATE
|
||||
|
||||
local pocket = {}
|
||||
|
||||
@@ -276,8 +276,8 @@ function pocket.init_nav(smem)
|
||||
|
||||
if (req_sv and not p_comms.is_sv_linked()) or (req_api and not p_comms.is_api_linked()) then
|
||||
-- report required connction(s)
|
||||
iocontrol.get_db().loader_require = { sv = req_sv, api = req_api }
|
||||
iocontrol.get_db().ps.toggle("loader_reqs")
|
||||
ioctl.get_db().loader_require = { sv = req_sv, api = req_api }
|
||||
ioctl.get_db().ps.toggle("loader_reqs")
|
||||
|
||||
-- bring up the app loader
|
||||
self.loader_return = app_id
|
||||
@@ -420,13 +420,12 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
nic.transmit(config.SVR_Channel, config.PKT_Channel, s_pkt)
|
||||
nic.transmit(config.SVR_Channel, config.PKT_Channel, frame)
|
||||
self.sv.seq_num = self.sv.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -434,13 +433,12 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_crd(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, frame)
|
||||
self.api.seq_num = self.api.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -448,13 +446,12 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
---@param msg_type CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_api(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.crdn_packet()
|
||||
local frame, crdn = comms.scada_frame(), comms.crdn_container()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable())
|
||||
crdn.make(msg_type, msg)
|
||||
frame.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, crdn.raw_packet())
|
||||
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
|
||||
nic.transmit(config.CRD_Channel, config.PKT_Channel, frame)
|
||||
self.api.seq_num = self.api.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -491,20 +488,28 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
function public.close_sv()
|
||||
sv_watchdog.cancel()
|
||||
nav.unload_sv()
|
||||
self.sv.linked = false
|
||||
|
||||
if self.sv.linked then
|
||||
self.sv.linked = false
|
||||
_send_sv(MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
self.sv.r_seq_num = nil
|
||||
self.sv.addr = comms.BROADCAST
|
||||
_send_sv(MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- close connection to coordinator API server
|
||||
function public.close_api()
|
||||
api_watchdog.cancel()
|
||||
nav.unload_api()
|
||||
self.api.linked = false
|
||||
|
||||
if self.api.linked then
|
||||
self.api.linked = false
|
||||
_send_crd(MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
self.api.r_seq_num = nil
|
||||
self.api.addr = comms.BROADCAST
|
||||
_send_crd(MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- close the connections to the servers
|
||||
@@ -515,24 +520,18 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
|
||||
-- attempt to re-link if any of the dependent links aren't active
|
||||
function public.link_update()
|
||||
if not self.sv.linked then
|
||||
if not (self.sv.linked and self.api.linked) then
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
|
||||
ioctl.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
|
||||
elseif self.sv.linked then
|
||||
ioctl.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
|
||||
ioctl.report_link_state(LINK_STATE.UNLINKED, false, false)
|
||||
end
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_sv_establish()
|
||||
self.establish_delay_counter = 4
|
||||
else
|
||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||
end
|
||||
elseif not self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_api_establish()
|
||||
if not self.api.linked then _send_api_establish() end
|
||||
if not self.sv.linked then _send_sv_establish() end
|
||||
self.establish_delay_counter = 4
|
||||
else
|
||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||
@@ -602,7 +601,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param auto_cfg [ PROCESS, number, number, number, number[] ]
|
||||
---@param auto_cfg auto_ctl_cfg
|
||||
function public.send_auto_start(auto_cfg)
|
||||
_send_api(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, table.unpack(auto_cfg) })
|
||||
end
|
||||
@@ -621,33 +620,25 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|crdn_frame|nil packet
|
||||
---@return mgmt_packet|crdn_packet|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local frame = nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt then
|
||||
-- get as SCADA management packet
|
||||
if s_pkt.protocol() == PROTOCOL.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() == PROTOCOL.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
local pkt = nil
|
||||
if frame then
|
||||
if frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_container().decode(frame)
|
||||
elseif frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
pkt = comms.crdn_container().decode(frame)
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
log.debug("attempted parse of illegal packet type " .. frame.protocol(), true)
|
||||
end
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
---@param packet mgmt_frame|crdn_frame
|
||||
---@param packet mgmt_packet|crdn_packet
|
||||
---@param length integer
|
||||
---@param max integer?
|
||||
---@return boolean
|
||||
@@ -660,17 +651,17 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
return ok
|
||||
end
|
||||
|
||||
---@param packet mgmt_frame|crdn_frame
|
||||
---@param packet mgmt_packet|crdn_packet
|
||||
local function _fail_type(packet)
|
||||
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: unrecognized packet type"
|
||||
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type))
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
---@param packet mgmt_packet|crdn_packet|nil
|
||||
function public.handle_packet(packet)
|
||||
local diag = iocontrol.get_db().diag
|
||||
local ps = iocontrol.get_db().ps
|
||||
local diag = ioctl.get_db().diag
|
||||
local ps = ioctl.get_db().ps
|
||||
|
||||
if packet ~= nil then
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
@@ -699,7 +690,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
api_watchdog.feed()
|
||||
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
---@cast packet crdn_packet
|
||||
if self.api.linked then
|
||||
if packet.type == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
@@ -708,13 +699,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
ioctl.get_db().facility.scram_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
iocontrol.get_db().facility.stop_ack(ack)
|
||||
ioctl.get_db().facility.stop_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
iocontrol.get_db().facility.start_ack(ack)
|
||||
ioctl.get_db().facility.start_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
ioctl.get_db().facility.ack_alarms_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
@@ -731,7 +722,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
local unit_id = packet.data[2]
|
||||
local ack = packet.data[3] == true
|
||||
|
||||
local unit = iocontrol.get_db().units[unit_id] ---@type pioctl_unit
|
||||
local unit = ioctl.get_db().units[unit_id] ---@type pkt_io_unit
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
@@ -749,38 +740,38 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_FAC then
|
||||
if _check_length(packet, 11) then
|
||||
iocontrol.rx.record_facility_data(packet.data)
|
||||
ioctl.rx.record_facility_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_FAC_DTL then
|
||||
if _check_length(packet, 12) then
|
||||
iocontrol.rx.record_fac_detail_data(packet.data)
|
||||
ioctl.rx.record_fac_detail_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
|
||||
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
|
||||
iocontrol.rx.record_unit_data(packet.data)
|
||||
if _check_length(packet, 12) and type(packet.data[1]) == "number" and ioctl.get_db().units[packet.data[1]] then
|
||||
ioctl.rx.record_unit_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_CTRL then
|
||||
if _check_length(packet, #iocontrol.get_db().units) then
|
||||
iocontrol.rx.record_control_data(packet.data)
|
||||
if _check_length(packet, #ioctl.get_db().units) then
|
||||
ioctl.rx.record_control_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_PROC then
|
||||
if _check_length(packet, #iocontrol.get_db().units + 1) then
|
||||
iocontrol.rx.record_process_data(packet.data)
|
||||
if _check_length(packet, #ioctl.get_db().units + 1) then
|
||||
ioctl.rx.record_process_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_WASTE then
|
||||
if _check_length(packet, #iocontrol.get_db().units + 1) then
|
||||
iocontrol.rx.record_waste_data(packet.data)
|
||||
if _check_length(packet, #ioctl.get_db().units + 1) then
|
||||
ioctl.rx.record_waste_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_RAD then
|
||||
if _check_length(packet, #iocontrol.get_db().units + 1) then
|
||||
iocontrol.rx.record_radiation_data(packet.data)
|
||||
if _check_length(packet, #ioctl.get_db().units + 1) then
|
||||
ioctl.rx.record_radiation_data(packet.data)
|
||||
end
|
||||
else _fail_type(packet) end
|
||||
else
|
||||
log.debug("discarding coordinator SCADA_CRDN packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
---@cast packet mgmt_packet
|
||||
if self.api.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
@@ -796,7 +787,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
|
||||
_send_api_keep_alive_ack(timestamp)
|
||||
|
||||
iocontrol.report_crd_tt(trip_time)
|
||||
ioctl.report_crd_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
@@ -820,19 +811,19 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
-- get configuration
|
||||
local conf = { num_units = fac_config[1], cooling = fac_config[2] }
|
||||
|
||||
iocontrol.init_fac(conf)
|
||||
ioctl.init_fac(conf)
|
||||
|
||||
log.info("coordinator connection established")
|
||||
self.establish_delay_counter = 0
|
||||
self.api.linked = true
|
||||
self.api.addr = src_addr
|
||||
|
||||
iocontrol.report_crd_link_error("")
|
||||
ioctl.report_crd_link_error("")
|
||||
|
||||
if self.sv.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
|
||||
ioctl.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, nil, self.api.addr)
|
||||
ioctl.report_link_state(LINK_STATE.API_LINK_ONLY, nil, self.api.addr)
|
||||
end
|
||||
else
|
||||
log.debug("invalid facility configuration table received from coordinator, establish failed")
|
||||
@@ -844,19 +835,19 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
log.info("coordinator connection denied")
|
||||
iocontrol.report_crd_link_error("denied")
|
||||
ioctl.report_crd_link_error("denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
log.info("coordinator connection denied due to collision")
|
||||
iocontrol.report_crd_link_error("collision")
|
||||
ioctl.report_crd_link_error("collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info("coordinator comms version mismatch")
|
||||
iocontrol.report_crd_link_error("comms version mismatch")
|
||||
ioctl.report_crd_link_error("comms version mismatch")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
|
||||
log.info("coordinator api version mismatch")
|
||||
iocontrol.report_crd_link_error("API version mismatch")
|
||||
ioctl.report_crd_link_error("API version mismatch")
|
||||
else
|
||||
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
||||
iocontrol.report_crd_link_error("unknown reply")
|
||||
ioctl.report_crd_link_error("unknown reply")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -893,7 +884,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
---@cast packet mgmt_packet
|
||||
if self.sv.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
@@ -909,7 +900,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
|
||||
_send_sv_keep_alive_ack(timestamp)
|
||||
|
||||
iocontrol.report_svr_tt(trip_time)
|
||||
ioctl.report_svr_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
@@ -963,7 +954,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.INFO_LIST_CMP then
|
||||
iocontrol.rx.record_network_data(packet.data)
|
||||
ioctl.rx.record_network_data(packet.data)
|
||||
else _fail_type(packet) end
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
@@ -976,27 +967,27 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
self.sv.linked = true
|
||||
self.sv.addr = src_addr
|
||||
|
||||
iocontrol.report_svr_link_error("")
|
||||
ioctl.report_svr_link_error("")
|
||||
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
|
||||
ioctl.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
|
||||
ioctl.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
|
||||
end
|
||||
else
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
log.info("supervisor connection denied")
|
||||
iocontrol.report_svr_link_error("denied")
|
||||
ioctl.report_svr_link_error("denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
log.info("supervisor connection denied due to collision")
|
||||
iocontrol.report_svr_link_error("collision")
|
||||
ioctl.report_svr_link_error("collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info("supervisor comms version mismatch")
|
||||
iocontrol.report_svr_link_error("comms version mismatch")
|
||||
ioctl.report_svr_link_error("comms version mismatch")
|
||||
else
|
||||
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||
iocontrol.report_svr_link_error("unknown reply")
|
||||
ioctl.report_svr_link_error("unknown reply")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ local U_CMD = comms.UNIT_COMMAND
|
||||
local process = {}
|
||||
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
io = nil, ---@type crd_io
|
||||
comms = nil ---@type pocket_comms
|
||||
}
|
||||
|
||||
-- initialize the process controller
|
||||
---@param iocontrol pocket_ioctl iocontrl system
|
||||
---@param ioctl pkt_io iocontrol system
|
||||
---@param pocket_comms pocket_comms pocket communications
|
||||
function process.init(iocontrol, pocket_comms)
|
||||
self.io = iocontrol
|
||||
function process.init(ioctl, pocket_comms)
|
||||
self.io = ioctl
|
||||
self.comms = pocket_comms
|
||||
end
|
||||
|
||||
@@ -125,11 +125,13 @@ end
|
||||
-- process start command
|
||||
---@param mode PROCESS process control mode
|
||||
---@param burn_target number burn rate target
|
||||
---@param range_start integer range control activation threshold
|
||||
---@param range_stop integer range control deactivation threshold
|
||||
---@param charge_target number charge level target
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits number[] unit burn rate limits
|
||||
function process.process_start(mode, burn_target, charge_target, gen_target, limits)
|
||||
self.comms.send_auto_start({ mode, burn_target, charge_target, gen_target, limits })
|
||||
function process.process_start(mode, burn_target, range_start, range_stop, charge_target, gen_target, limits)
|
||||
self.comms.send_auto_start({ mode, burn_target, range_start, range_stop, charge_target, gen_target, limits })
|
||||
log.debug("PROCESS: START AUTO CTRL")
|
||||
end
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local configure = require("pocket.configure")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
local threads = require("pocket.threads")
|
||||
|
||||
local POCKET_VERSION = "v1.0.3"
|
||||
local POCKET_VERSION = "v1.1.0"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -79,7 +79,7 @@ local function main()
|
||||
ppm.mount_all()
|
||||
|
||||
-- record version for GUI
|
||||
iocontrol.get_db().version = POCKET_VERSION
|
||||
ioctl.get_db().version = POCKET_VERSION
|
||||
|
||||
----------------------------------------
|
||||
-- memory allocation
|
||||
@@ -132,7 +132,7 @@ local function main()
|
||||
network.init_mac(config.AuthKey)
|
||||
end
|
||||
|
||||
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
||||
ioctl.report_link_state(ioctl.LINK_STATE.UNLINKED)
|
||||
|
||||
-- get the communications modem
|
||||
if smem_dev.modem == nil then
|
||||
@@ -154,7 +154,7 @@ local function main()
|
||||
log.debug("startup> comms init")
|
||||
|
||||
-- init I/O control
|
||||
iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
|
||||
ioctl.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
|
||||
@@ -11,8 +11,8 @@ local core = require("graphics.core")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
|
||||
local RENDER_SLEEP = 100 -- 100ms, 2 ticks
|
||||
|
||||
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA
|
||||
|
||||
@@ -38,6 +38,25 @@ function threads.thread__main(smem)
|
||||
local sv_wd = smem.pkt_sys.sv_wd
|
||||
local api_wd = smem.pkt_sys.api_wd
|
||||
local nav = smem.pkt_sys.nav
|
||||
local nic = smem.pkt_sys.nic
|
||||
|
||||
-- main loop periodic tasks
|
||||
local function loop_tick()
|
||||
-- relink if necessary
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- update any tasks for the active page
|
||||
if nav.get_current_page() then
|
||||
local page_tasks = nav.get_current_page().tasks
|
||||
for i = 1, #page_tasks do page_tasks[i]() end
|
||||
end
|
||||
|
||||
-- NIC periodic link-layer tasks
|
||||
nic.periodic()
|
||||
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
end
|
||||
|
||||
-- start connection watchdogs
|
||||
sv_wd.feed()
|
||||
@@ -49,37 +68,27 @@ function threads.thread__main(smem)
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- relink if necessary
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- update any tasks for the active page
|
||||
if nav.get_current_page() then
|
||||
local page_tasks = nav.get_current_page().tasks
|
||||
for i = 1, #page_tasks do page_tasks[i]() end
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif sv_wd.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log.info("supervisor server timeout")
|
||||
pocket_comms.close_sv()
|
||||
elseif api_wd.is_timer(param1) then
|
||||
-- coordinator watchdog timeout
|
||||
log.info("coordinator api server timeout")
|
||||
pocket_comms.close_api()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
if event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
pocket_comms.handle_packet(packet)
|
||||
elseif event == "timer" then
|
||||
-- pass this timer event onto the right handler
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
loop_tick()
|
||||
elseif sv_wd.is_timer(param1) then
|
||||
-- supervisor connection timed out
|
||||
log.info("supervisor server timeout")
|
||||
pocket_comms.close_sv()
|
||||
elseif api_wd.is_timer(param1) then
|
||||
-- coordinator connection timed out
|
||||
log.info("coordinator api server timeout")
|
||||
pocket_comms.close_api()
|
||||
else
|
||||
-- notify timer callback dispatcher, no other handler claimed this event
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a mouse event
|
||||
@@ -156,9 +165,7 @@ function threads.thread__render(smem)
|
||||
local msg = render_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
if msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
local cmd = msg.message ---@type queue_data
|
||||
|
||||
@@ -177,8 +184,6 @@ function threads.thread__render(smem)
|
||||
if type(cmd.val[2]) == "function" then cmd.val[2]() end
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ local util = require("scada-common.util")
|
||||
|
||||
local lockbox = require("lockbox")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -27,9 +27,9 @@ local APP_ID = pocket.APP_ID
|
||||
-- create about page view
|
||||
---@param root Container parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.ABOUT, frame)
|
||||
|
||||
@@ -38,7 +38,7 @@ local function create_pages(root)
|
||||
local fw_page = app.new_page(about_page, 3)
|
||||
local hw_page = app.new_page(about_page, 4)
|
||||
|
||||
local about = Div{parent=frame,x=1,y=2}
|
||||
local about = Div{parent=frame,y=2}
|
||||
|
||||
TextBox{parent=about,y=1,text="System Information",alignment=ALIGN.CENTER}
|
||||
|
||||
@@ -54,7 +54,7 @@ local function create_pages(root)
|
||||
|
||||
local config = pocket.config
|
||||
|
||||
local nt_div = Div{parent=frame,x=1,y=2}
|
||||
local nt_div = Div{parent=frame,y=2}
|
||||
TextBox{parent=nt_div,y=1,text="Network Details",alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
@@ -83,14 +83,14 @@ local function create_pages(root)
|
||||
|
||||
--#region Firmware Versions
|
||||
|
||||
local fw_div = Div{parent=frame,x=1,y=2}
|
||||
local fw_div = Div{parent=frame,y=2}
|
||||
TextBox{parent=fw_div,y=1,text="Firmware Versions",alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
local fw_list_box = ListBox{parent=fw_div,x=1,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local fw_list_box = ListBox{parent=fw_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
|
||||
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
|
||||
local fw_list = Div{parent=fw_list_box,y=2,height=18}
|
||||
|
||||
TextBox{parent=fw_list,x=2,text="Pocket Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version}
|
||||
@@ -119,7 +119,7 @@ local function create_pages(root)
|
||||
|
||||
--#region Host Versions
|
||||
|
||||
local hw_div = Div{parent=frame,x=1,y=2}
|
||||
local hw_div = Div{parent=frame,y=2}
|
||||
TextBox{parent=hw_div,y=1,text="Host Versions",alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
@@ -134,7 +134,7 @@ local function create_pages(root)
|
||||
|
||||
--#endregion
|
||||
|
||||
local root_pane = MultiPane{parent=frame,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
|
||||
local root_pane = MultiPane{parent=frame,y=1,panes={about,nt_div,fw_div,hw_div}}
|
||||
|
||||
app.set_root_pane(root_pane)
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Alarm Test App
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -30,15 +30,15 @@ local c_blue_gray = cpair(colors.blue, colors.gray)
|
||||
-- create alarm test page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
local ps = db.ps
|
||||
local ttest = db.diag.tone_test
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.ALARMS, frame, nil, true)
|
||||
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
local page_div = Div{parent=main,y=2,width=main.get_width()}
|
||||
|
||||
--#region alarm testing
|
||||
@@ -163,12 +163,12 @@ local function new_view(root)
|
||||
|
||||
TextBox{parent=info_div,x=2,y=1,text="This app provides tools to test alarm sounds by alarm and by tone (1-8)."}
|
||||
TextBox{parent=info_div,x=2,y=6,text="The system must be idle (all units stopped with no alarms active) for testing to run."}
|
||||
TextBox{parent=info_div,x=2,y=12,text="Currently, testing will be denied unless you have a Facility Authentication Key set (this will change in the future)."}
|
||||
TextBox{parent=info_div,x=2,y=12,text="Testing will be denied unless you enabled it in the Supervisor's configuration."}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes={alarms_div,tones_div,info_div}}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes={alarms_div,tones_div,info_div}}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
local list = {
|
||||
|
||||
@@ -6,7 +6,7 @@ local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
@@ -37,19 +37,19 @@ local box_label = cpair(colors.lightGray, colors.gray)
|
||||
-- new computer list page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.COMPS, frame, nil, true, false)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -251,7 +251,7 @@ local function new_view(root)
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
-- setup sidebar
|
||||
@@ -285,7 +285,7 @@ local function new_view(root)
|
||||
load_pane.set_value(1)
|
||||
|
||||
-- clear the list of connected computers so that connections re-appear on reload of this app
|
||||
iocontrol.rx.clear_comp_record()
|
||||
ioctl.rx.clear_comp_record()
|
||||
end
|
||||
|
||||
app.set_load(load)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
local process = require("pocket.process")
|
||||
|
||||
@@ -49,19 +49,19 @@ local hzd_dis_colors = style.hzd_dis_colors
|
||||
local function new_view(root)
|
||||
local btn_fg_bg = cpair(colors.green, colors.black)
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.CONTROL, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.green,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -126,13 +126,13 @@ local function new_view(root)
|
||||
u_page.tasks = { update }
|
||||
|
||||
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
|
||||
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
|
||||
PushButton{parent=u_div,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
|
||||
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
|
||||
|
||||
local rate = DataIndicator{parent=u_div,y=3,lu_colors=lu_col,label="Burn",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
|
||||
local ctrl = IconIndicator{parent=u_div,x=1,y=6,label="Control State",states=mode_states}
|
||||
local ctrl = IconIndicator{parent=u_div,y=6,label="Control State",states=mode_states}
|
||||
|
||||
rate.register(u_ps, "act_burn_rate", rate.update)
|
||||
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
|
||||
@@ -201,7 +201,7 @@ local function new_view(root)
|
||||
db.facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
set_sidebar()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
@@ -41,19 +41,19 @@ local grn_ind_s = style.icon_states.grn_ind_s
|
||||
-- new unit page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.FACILITY, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -100,7 +100,7 @@ local function new_view(root)
|
||||
|
||||
TextBox{parent=f_div,y=8,text="Induction Matrix",alignment=ALIGN.CENTER}
|
||||
|
||||
local eta = TextBox{parent=f_div,x=1,y=10,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,colors.gray)}
|
||||
local eta = TextBox{parent=f_div,y=10,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,colors.gray)}
|
||||
eta.register(fac.induction_ps_tbl[1], "eta_string", eta.set_value)
|
||||
|
||||
TextBox{parent=f_div,y=12,text="Unit Statuses",alignment=ALIGN.CENTER}
|
||||
@@ -179,7 +179,7 @@ local function new_view(root)
|
||||
if fac.tank_list[t] == 1 then
|
||||
t_div.line_break()
|
||||
|
||||
local tank = IconIndicator{parent=t_div,x=1,label="Unit Tank "..t.." (U-"..t..")",states=basic_states}
|
||||
local tank = IconIndicator{parent=t_div,label="Unit Tank "..t.." (U-"..t..")",states=basic_states}
|
||||
tank.register(db.units[t].tank_ps_tbl[1], "DynamicTankStatus", tank.update)
|
||||
|
||||
TextBox{parent=t_div,x=5,text="\x07 Unit "..t,fg_bg=label_fg_bg}
|
||||
@@ -188,7 +188,7 @@ local function new_view(root)
|
||||
|
||||
t_div.line_break()
|
||||
|
||||
local tank = IconIndicator{parent=t_div,x=1,label="Fac. Tank "..f_tank_id.." (F-"..f_tank_id..")",states=basic_states}
|
||||
local tank = IconIndicator{parent=t_div,label="Fac. Tank "..f_tank_id.." (F-"..f_tank_id..")",states=basic_states}
|
||||
tank.register(fac.tank_ps_tbl[f_tank_id], "DynamicTankStatus", tank.update)
|
||||
|
||||
local connections = ""
|
||||
@@ -211,7 +211,7 @@ local function new_view(root)
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local f_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local f_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(f_pane)
|
||||
|
||||
-- setup sidebar
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local docs = require("pocket.ui.docs")
|
||||
@@ -33,14 +33,14 @@ local APP_ID = pocket.APP_ID
|
||||
-- new system guide view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.GUIDE, frame)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)}
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
@@ -53,7 +53,7 @@ local function new_view(root)
|
||||
load_text_2.set_value(b or "")
|
||||
end
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
local btn_fg_bg = cpair(colors.cyan, colors.black)
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
@@ -110,13 +110,13 @@ local function new_view(root)
|
||||
|
||||
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
|
||||
|
||||
local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)}
|
||||
local query_field = TextField{parent=search,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
local func_ref = {}
|
||||
|
||||
PushButton{parent=search,x=20,y=3,text="GO",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()func_ref.run_search()end}
|
||||
|
||||
local search_results = ListBox{parent=search,x=1,y=5,scroll_height=200,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local search_results = ListBox{parent=search,y=5,scroll_height=200,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
|
||||
function func_ref.run_search()
|
||||
local query = string.lower(query_field.get_value())
|
||||
@@ -239,7 +239,7 @@ local function new_view(root)
|
||||
PushButton{parent=coord_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
|
||||
|
||||
load_text(false, "Main Display")
|
||||
local main_disp_page = guide_section(sect_construct_data, coord_page, "Main Display", docs.c_ui.main, 300)
|
||||
local main_disp_page = guide_section(sect_construct_data, coord_page, "Main Display", docs.c_ui.main, 310)
|
||||
load_text(false, "Flow Display")
|
||||
local flow_disp_page = guide_section(sect_construct_data, coord_page, "Flow Display", docs.c_ui.flow, 210)
|
||||
load_text(false, "Unit Displays")
|
||||
@@ -255,7 +255,7 @@ local function new_view(root)
|
||||
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
load_text(false, "Common Items")
|
||||
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
|
||||
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 130)
|
||||
load_text(false, "Reactor PLC")
|
||||
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 190)
|
||||
load_text(false, "RTU Gateway")
|
||||
@@ -285,7 +285,7 @@ local function new_view(root)
|
||||
load_text("Links")
|
||||
|
||||
TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
PushButton{parent=lnk,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="GitHub",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
@@ -298,7 +298,7 @@ local function new_view(root)
|
||||
TextBox{parent=lnk,text="discord.gg/R9NSCkhcwt"}
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
-- link help resources
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Loading Screen App
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||
@@ -15,22 +15,22 @@ local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
local LINK_STATE = ioctl.LINK_STATE
|
||||
|
||||
-- create the connecting to SV & API page
|
||||
---@param root Container parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
local main = Div{parent=root,y=1}
|
||||
|
||||
db.nav.register_app(APP_ID.LOADER, main).new_page(nil, function () end)
|
||||
|
||||
local conn_sv_wait = conn_waiting(main, 6, false)
|
||||
local conn_api_wait = conn_waiting(main, 6, true)
|
||||
local main_pane = Div{parent=main,x=1,y=2}
|
||||
local main_pane = Div{parent=main,y=2}
|
||||
|
||||
local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
|
||||
local root_pane = MultiPane{parent=main,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
|
||||
|
||||
local function update()
|
||||
local state = db.ps.get("link_state")
|
||||
@@ -56,7 +56,7 @@ local function create_pages(root)
|
||||
root_pane.register(db.ps, "link_state", update)
|
||||
root_pane.register(db.ps, "loader_reqs", update)
|
||||
|
||||
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
|
||||
TextBox{parent=main_pane,text="Connected!",y=6,alignment=core.ALIGN.CENTER}
|
||||
end
|
||||
|
||||
return create_pages
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
local process = require("pocket.process")
|
||||
|
||||
@@ -27,6 +27,8 @@ local NumberField = require("graphics.elements.form.NumberField")
|
||||
|
||||
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
|
||||
|
||||
local PROCESS = types.PROCESS
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
@@ -50,19 +52,19 @@ local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
-- new process control page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.PROCESS, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.purple,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -77,7 +79,7 @@ local function new_view(root)
|
||||
local panes = {} ---@type Div[]
|
||||
|
||||
-- create all page divs
|
||||
for _ = 1, db.facility.num_units + 3 do
|
||||
for _ = 1, db.facility.num_units + 4 do
|
||||
local div = Div{parent=page_div}
|
||||
table.insert(panes, div)
|
||||
end
|
||||
@@ -107,7 +109,7 @@ local function new_view(root)
|
||||
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=u_div,y=3,text="Auto Rate Limit",fg_bg=label_fg_bg}
|
||||
rate_limits[i] = NumberField{parent=u_div,x=1,y=4,width=16,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
rate_limits[i] = NumberField{parent=u_div,y=4,width=16,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=u_div,x=18,y=4,text="mB/t",width=4,fg_bg=label_fg_bg}
|
||||
|
||||
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
|
||||
@@ -151,36 +153,82 @@ local function new_view(root)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region process control options page
|
||||
--#region process control mode page
|
||||
|
||||
local o_pane = panes[db.facility.num_units + 2]
|
||||
local o_div = Div{parent=o_pane,x=2,width=main.get_width()-2}
|
||||
local m_pane = panes[db.facility.num_units + 3]
|
||||
local m_div = Div{parent=m_pane,x=2,width=main.get_width()-2}
|
||||
|
||||
local opt_page = app.new_page(nil, db.facility.num_units + 2)
|
||||
opt_page.tasks = { update }
|
||||
local mode_page = app.new_page(nil, db.facility.num_units + 3)
|
||||
mode_page.tasks = { update }
|
||||
|
||||
TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER}
|
||||
TextBox{parent=m_div,y=1,text="Process Mode",alignment=ALIGN.CENTER}
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||
local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
|
||||
local desc = TextBox{parent=m_div,y=9,height=9,text="",fg_bg=label_fg_bg}
|
||||
|
||||
local function _set_desc(m)
|
||||
if m == PROCESS.MAX_BURN then
|
||||
desc.set_value("This mode runs all assigned reactors at their configured unit auto rate limits.")
|
||||
elseif m == PROCESS.BURN_RATE then
|
||||
desc.set_value("This mode runs assigned reactors by priority group to meet the requested burn rate. Primary ones are used, then secondary if they can't keep up, etc.")
|
||||
elseif m == PROCESS.CHARGE then
|
||||
desc.set_value("This mode runs assigned reactors by priority group to meet the requested induction matrix charge level. Primary ones are used, then secondary if they can't keep up, etc.")
|
||||
elseif m == PROCESS.GEN_RATE then
|
||||
desc.set_value("This mode runs assigned reactors by priority group to meet the requested energy generation rate. Primary ones are used, then secondary if they can't keep up, etc.")
|
||||
elseif m == PROCESS.RANGE_CONTROL then
|
||||
desc.set_value("This mode runs all assigned reactors at their configured unit auto rate limits once charge drops below the start percentage until it meets the stop percentage.")
|
||||
else
|
||||
desc.set_value("Unknown mode selected.")
|
||||
end
|
||||
end
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate", "Charge Range" }
|
||||
local mode = RadioButton{parent=m_div,y=3,options=ctl_opts,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,callback=_set_desc,dis_fg_bg=style.btn_disable}
|
||||
|
||||
mode.register(f_ps, "process_mode", mode.set_value)
|
||||
desc.register(f_ps, "process_mode", _set_desc)
|
||||
|
||||
TextBox{parent=o_div,y=9,text="Burn Rate Target",fg_bg=label_fg_bg}
|
||||
local b_target = NumberField{parent=o_div,x=1,y=10,width=15,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=o_div,x=17,y=10,text="mB/t",fg_bg=label_fg_bg}
|
||||
--#endregion
|
||||
|
||||
TextBox{parent=o_div,y=12,text="Charge Level Target",fg_bg=label_fg_bg}
|
||||
local c_target = NumberField{parent=o_div,x=1,y=13,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=o_div,x=17,y=13,text="M"..db.energy_label,fg_bg=label_fg_bg}
|
||||
--#region process control setpoints page
|
||||
|
||||
TextBox{parent=o_div,y=15,text="Generation Target",fg_bg=label_fg_bg}
|
||||
local g_target = NumberField{parent=o_div,x=1,y=16,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=o_div,x=17,y=16,text="k"..db.energy_label.."/t",fg_bg=label_fg_bg}
|
||||
local s_pane = panes[db.facility.num_units + 4]
|
||||
local s_div = Div{parent=s_pane,x=2,width=main.get_width()-2}
|
||||
|
||||
local sp_page = app.new_page(nil, db.facility.num_units + 4)
|
||||
sp_page.tasks = { update }
|
||||
|
||||
TextBox{parent=s_div,y=1,text="Process Setpoints",alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=s_div,y=3,text="Burn Rate Target",fg_bg=label_fg_bg}
|
||||
local b_target = NumberField{parent=s_div,y=4,width=15,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=s_div,x=17,y=4,text="mB/t",fg_bg=label_fg_bg}
|
||||
|
||||
TextBox{parent=s_div,y=6,text="Charge Level Target",fg_bg=label_fg_bg}
|
||||
local c_target = NumberField{parent=s_div,y=7,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=s_div,x=17,y=7,text="M"..db.energy_label,fg_bg=label_fg_bg}
|
||||
|
||||
TextBox{parent=s_div,y=9,text="Generation Target",fg_bg=label_fg_bg}
|
||||
local g_target = NumberField{parent=s_div,y=10,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=s_div,x=17,y=10,text="k"..db.energy_label.."/t",fg_bg=label_fg_bg}
|
||||
|
||||
local range_start, range_stop ---@type NumberField, NumberField
|
||||
|
||||
local function _update_start_val(value) range_start.set_value(math.min(range_start.get_value(), value - 1)) end
|
||||
local function _update_stop_val(value) range_stop.set_value(math.max(range_stop.get_value(), value + 1)) end
|
||||
|
||||
TextBox{parent=s_div,y=12,text="Charge Range - Start",fg_bg=label_fg_bg}
|
||||
range_start = NumberField{parent=s_div,y=13,width=15,default=0,min=0,max=99,align_right=true,on_unfocus=_update_stop_val,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=s_div,x=17,y=13,text="%",fg_bg=label_fg_bg}
|
||||
|
||||
TextBox{parent=s_div,y=15,text="Charge Range - Stop",fg_bg=label_fg_bg}
|
||||
range_stop = NumberField{parent=s_div,y=16,width=15,default=0,min=1,max=100,align_right=true,on_unfocus=_update_start_val,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
|
||||
TextBox{parent=s_div,x=17,y=16,text="%",fg_bg=label_fg_bg}
|
||||
|
||||
b_target.register(f_ps, "process_burn_target", b_target.set_value)
|
||||
c_target.register(f_ps, "process_charge_target", c_target.set_value)
|
||||
g_target.register(f_ps, "process_gen_target", g_target.set_value)
|
||||
range_start.register(f_ps, "process_range_start", range_start.set_value)
|
||||
range_stop.register(f_ps, "process_range_stop", range_stop.set_value)
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -194,9 +242,9 @@ local function new_view(root)
|
||||
|
||||
TextBox{parent=c_div,y=1,text="Process Control",alignment=ALIGN.CENTER}
|
||||
|
||||
local u_stat = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,x=1,y=3,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",alignment=ALIGN.CENTER}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",height=2,alignment=ALIGN.CENTER,trim_whitespace=true,fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
local u_stat = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,y=3,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,y=1,text="UNKNOWN",alignment=ALIGN.CENTER}
|
||||
local stat_line_2 = TextBox{parent=u_stat,y=2,text="awaiting data...",height=2,alignment=ALIGN.CENTER,trim_whitespace=true,fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
|
||||
stat_line_1.register(f_ps, "status_line_1", stat_line_1.set_value)
|
||||
stat_line_2.register(f_ps, "status_line_2", stat_line_2.set_value)
|
||||
@@ -205,8 +253,13 @@ local function new_view(root)
|
||||
local limits = {}
|
||||
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_numeric() end
|
||||
|
||||
process.process_start(mode.get_value(), b_target.get_numeric(), db.energy_convert_to_fe(c_target.get_numeric()),
|
||||
db.energy_convert_to_fe(g_target.get_numeric()), limits)
|
||||
-- make sure stop is always above start (start maxes at 99 and stop maxes at 100 so this always works)
|
||||
if range_stop.get_value() <= range_start.get_value() then
|
||||
range_stop.set_value(range_start.get_value() + 1)
|
||||
end
|
||||
|
||||
process.process_start(mode.get_value(), b_target.get_numeric(), range_start.get_numeric(), range_stop.get_numeric(),
|
||||
db.energy_convert_to_fe(c_target.get_numeric()), db.energy_convert_to_fe(g_target.get_numeric()), limits)
|
||||
end
|
||||
|
||||
local start = HazardButton{parent=c_div,x=2,y=9,text="START",accent=colors.lightBlue,callback=_start_auto,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
|
||||
@@ -237,6 +290,8 @@ local function new_view(root)
|
||||
b_target.disable()
|
||||
c_target.disable()
|
||||
g_target.disable()
|
||||
range_start.disable()
|
||||
range_stop.disable()
|
||||
|
||||
mode.disable()
|
||||
start.disable()
|
||||
@@ -246,6 +301,8 @@ local function new_view(root)
|
||||
b_target.enable()
|
||||
c_target.enable()
|
||||
g_target.enable()
|
||||
range_start.enable()
|
||||
range_stop.enable()
|
||||
|
||||
mode.enable()
|
||||
if db.facility.auto_ready then start.enable() end
|
||||
@@ -258,10 +315,10 @@ local function new_view(root)
|
||||
|
||||
--#region auto-SCRAM annunciator page
|
||||
|
||||
local a_pane = panes[db.facility.num_units + 3]
|
||||
local a_pane = panes[db.facility.num_units + 2]
|
||||
local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}
|
||||
|
||||
local annunc_page = app.new_page(nil, db.facility.num_units + 3)
|
||||
local annunc_page = app.new_page(nil, db.facility.num_units + 2)
|
||||
annunc_page.tasks = { update }
|
||||
|
||||
TextBox{parent=a_div,y=1,text="Automatic SCRAM",alignment=ALIGN.CENTER}
|
||||
@@ -291,7 +348,7 @@ local function new_view(root)
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
-- setup sidebar
|
||||
@@ -300,7 +357,8 @@ local function new_view(root)
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
|
||||
{ label = " \x17 ", color = core.cpair(colors.black, colors.purple), callback = proc_ctrl.nav_to },
|
||||
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = annunc_page.nav_to },
|
||||
{ label = "OPT", color = core.cpair(colors.black, colors.yellow), callback = opt_page.nav_to }
|
||||
{ label = " \x07 ", color = core.cpair(colors.black, colors.yellow), callback = mode_page.nav_to },
|
||||
{ label = " \x12 ", color = core.cpair(colors.black, colors.lightBlue), callback = sp_page.nav_to }
|
||||
}
|
||||
|
||||
for i = 1, db.facility.num_units do
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
@@ -33,19 +33,19 @@ local lu_col = style.label_unit_pair
|
||||
-- new radiation monitor page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.RADMON, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -174,7 +174,7 @@ local function new_view(root)
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
-- setup sidebar
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
@@ -49,19 +49,19 @@ local emc_ind_s = {
|
||||
-- new unit page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.UNITS, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -149,7 +149,7 @@ local function new_view(root)
|
||||
u_page.tasks = { update }
|
||||
|
||||
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
|
||||
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
|
||||
PushButton{parent=u_div,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
|
||||
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
|
||||
|
||||
local type = util.trinary(unit.num_boilers > 0, "Sodium Cooled Reactor", "Boiling Water Reactor")
|
||||
@@ -158,7 +158,7 @@ local function new_view(root)
|
||||
local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Burn",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
|
||||
|
||||
local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states}
|
||||
local ctrl = IconIndicator{parent=u_div,y=8,label="Control State",states=mode_states}
|
||||
|
||||
rate.register(u_ps, "act_burn_rate", rate.update)
|
||||
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
|
||||
@@ -166,24 +166,24 @@ local function new_view(root)
|
||||
|
||||
u_div.line_break()
|
||||
|
||||
local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states}
|
||||
local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states}
|
||||
local rct = IconIndicator{parent=u_div,label="Fission Reactor",states=basic_states}
|
||||
local rps = IconIndicator{parent=u_div,label="Protection System",states=basic_states}
|
||||
|
||||
rct.register(u_ps, "U_ReactorStatus", rct.update)
|
||||
rps.register(u_ps, "U_RPS", rps.update)
|
||||
|
||||
u_div.line_break()
|
||||
|
||||
local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states}
|
||||
local rcs = IconIndicator{parent=u_div,label="Coolant System",states=basic_states}
|
||||
rcs.register(u_ps, "U_RCS", rcs.update)
|
||||
|
||||
for b = 1, unit.num_boilers do
|
||||
local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states}
|
||||
local blr = IconIndicator{parent=u_div,label="Boiler "..b,states=basic_states}
|
||||
blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update)
|
||||
end
|
||||
|
||||
for t = 1, unit.num_turbines do
|
||||
local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states}
|
||||
local tbn = IconIndicator{parent=u_div,label="Turbine "..t,states=basic_states}
|
||||
tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update)
|
||||
end
|
||||
|
||||
@@ -377,7 +377,7 @@ local function new_view(root)
|
||||
end
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local u_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
set_sidebar(active_unit)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
local process = require("pocket.process")
|
||||
|
||||
@@ -40,19 +40,19 @@ local wht_ind_s = style.icon_states.wht_ind_s
|
||||
-- new waste control page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,y=1}
|
||||
local frame = Div{parent=root,y=1}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.WASTE, frame, nil, false, true)
|
||||
|
||||
local load_div = Div{parent=frame,x=1,y=1}
|
||||
local main = Div{parent=frame,x=1,y=1}
|
||||
local load_div = Div{parent=frame,y=1}
|
||||
local main = Div{parent=frame,y=1}
|
||||
|
||||
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
|
||||
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.brown,colors._INHERIT)}
|
||||
|
||||
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
|
||||
local load_pane = MultiPane{parent=main,y=1,panes={load_div,main}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
|
||||
|
||||
@@ -262,7 +262,7 @@ local function new_view(root)
|
||||
--#endregion
|
||||
|
||||
-- setup multipane
|
||||
local w_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
local w_pane = MultiPane{parent=page_div,y=1,panes=panes}
|
||||
app.set_root_pane(w_pane)
|
||||
|
||||
-- setup sidebar
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Connection Waiting Spinner
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -22,10 +22,10 @@ local cpair = core.cpair
|
||||
---@param y integer y offset
|
||||
local function init(parent, y, is_api)
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=1,y=1}
|
||||
local root = Div{parent=parent,y=1}
|
||||
|
||||
-- bounding box div
|
||||
local box = Div{parent=root,x=1,y=y,height=12}
|
||||
local box = Div{parent=root,y=y,height=12}
|
||||
|
||||
local waiting_x = math.floor(parent.get_width() / 2) - 1
|
||||
|
||||
@@ -34,11 +34,11 @@ local function init(parent, y, is_api)
|
||||
if is_api then
|
||||
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||
TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg),trim_whitespace=true}
|
||||
msg.register(iocontrol.get_db().ps, "api_link_msg", msg.set_value)
|
||||
msg.register(ioctl.get_db().ps, "api_link_msg", msg.set_value)
|
||||
else
|
||||
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
|
||||
TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg),trim_whitespace=true}
|
||||
msg.register(iocontrol.get_db().ps, "svr_link_msg", msg.set_value)
|
||||
msg.register(ioctl.get_db().ps, "svr_link_msg", msg.set_value)
|
||||
end
|
||||
|
||||
return root
|
||||
|
||||
@@ -95,7 +95,7 @@ text("A wired modem is only connected to the block when you right click it and i
|
||||
tip("Do not connect all peripherals in the system on the same network cable, since Reactor PLCs will grab the first reactor they find and you may accidentally duplicate RTUs.")
|
||||
sect("Computer Conns")
|
||||
tip("It helps to be familiar with how ComputerCraft manages peripherals before using this system, though it is not necessary.")
|
||||
doc("usage_conn_network", "Network", "All computers in the system communicate with each other via wireless or ender modems. Ender modems are preferred due to the unlimited range.")
|
||||
doc("usage_conn_network", "Network", "All computers in the system communicate with each other via wired, wireless, and/or Ender modems. Ender modems are preferred over wireless due to the unlimited range.")
|
||||
text("Five different network channels are used and must have the same value for each name across all devices.")
|
||||
text("For example, the supervisor channel SVR_CHANNEL must be set to the same channel for all devices in your system. Two different named channels should not share the same value (such as SVR_CHANNEL vs CRD_CHANNEL).")
|
||||
doc("usage_conn_peri", "Peripherals", "ComputerCraft peripherals like monitors and speakers need to touch the computer or be connected via wired modems.")
|
||||
@@ -167,10 +167,11 @@ doc("usage_auto_setpoints", "Setpoints", "Three setpoint spinner inputs are avai
|
||||
doc("usage_auto_limits", "Unit Limits", "Each unit can be limited to a maximum auto control burn rate to prevent exceeding any safe levels that you know of.")
|
||||
doc("usage_auto_states", "Unit States", "Any assigned units must be shown as Ready and not Degraded to use auto control. See Operator UIs > Coordinator > Main Display for more.")
|
||||
sect("Operation Modes")
|
||||
text("Four auto control modes are available that function based on configurations set on the main display. All modes except Monitored Max Burn will try to only use the primary group until it can't keep up, then the secondary, etc.")
|
||||
text("Five auto control modes are available that function based on configurations set on the main display. All modes except Monitored Max Burn and Charge Range will try to only use the primary group until it can't keep up, then the secondary, etc.")
|
||||
note("No units will be set to a burn rate higher than their limit.")
|
||||
doc("usage_op_mon_max", "Monitored Max Burn", "This mode runs all units assigned to auto control at their unit limit burn rate regardless of priority group.")
|
||||
doc("usage_op_com_rate", "Combined Burn Rate", "Assigned units will be commanded to meet the Burn Target setpoint.")
|
||||
doc("usage_op_chg_range", "Charge Range", "This mode runs all units assigned to auto control at their unit limit burn rate regardless of priority group if the charge percentage drops to the start threshold until it reaches the stop threshold, keeping it within that range.")
|
||||
doc("usage_op_chg_level", "Charge Level", "Assigned units will be commanded to bring the induction matrix up to the requested Charge Target.")
|
||||
doc("usage_op_gen_rate", "Generation Rate", "Assigned units will be commanded to maintain the requested Generation Target.")
|
||||
note("The rate used is the input rate into the induction matrix, so using other power generation sources may disrupt this control mode.")
|
||||
@@ -324,7 +325,9 @@ doc("ui_fac_rad", "Radiation", "The facility radiation, which is the current max
|
||||
doc("ui_fac_linked", "Linked RTUs", "The number of RTU Gateways connected.")
|
||||
sect("Automatic Control")
|
||||
text("This interface is used for managing automatic facility control, which only applies to units set via the unit display to be under auto control. This includes setpoints, status, configuration, and control.")
|
||||
doc("ui_fac_auto_alt", "\x12T/\x12R", "This selector next to Charge Target/Range lets you toggle between charge target and charge range control.")
|
||||
doc("ui_fac_auto_bt", "Burn Target", "When set to Combined Burn Rate mode, assigned units will ramp up to meet this combined target.")
|
||||
doc("ui_fac_auto_cr", "Charge Range", "When set to Charge Range mode, assigned units will run once the induction matrix charge percentage falls to the start threshold until it reaches the stop threshold, keeping it within the range.")
|
||||
doc("ui_fac_auto_ct", "Charge Target", "When set to Charge Level mode, assigned units will run to reach and maintain this induction matrix charge level.")
|
||||
doc("ui_fac_auto_gt", "Gen. Target", "When set to Generation Rate mode, assigned units will run to reach and maintain this continuous power output, using the induction matrix input rate.")
|
||||
doc("ui_fac_save", "SAVE", "This saves your configuration without starting control.")
|
||||
@@ -427,15 +430,21 @@ sect("Core Status")
|
||||
doc("fp_status", "STATUS", "This is always lit, except on the Reactor PLC (see Reactor PLC section).")
|
||||
doc("fp_heartbeat", "HEARTBEAT", "This alternates between lit and unlit as the main loop on the device runs. If this freezes, something is wrong and the logs will indicate why.")
|
||||
sect("Hardware & Network")
|
||||
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the Supervisor's connection lists.")
|
||||
doc("fp_modem", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.")
|
||||
doc("fp_modem", "MODEM", "This indicates the status of the comms modem, if you only have one. If you have two, they will be named WD/WL MODEM.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "link down", "link up" }, { colors.gray, colors.yellow, colors.green })
|
||||
doc("fp_modem", "WD MODEM", "This indicates the status of the wired comms modem.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "link down", "link up" }, { colors.gray, colors.yellow, colors.green })
|
||||
doc("fp_modem", "WL MODEM", "This indicates the status of the wireless/Ender comms modem.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "link down", "link up" }, { colors.gray, colors.yellow, colors.green })
|
||||
doc("fp_network", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.")
|
||||
list(DOC_LIST_TYPE.LED, { "not linked", "linked", "link denied", "bad comms version", "duplicate PLC" }, { colors.gray, colors.green, colors.red, colors.orange, colors.yellow })
|
||||
text("You can fix \"bad comms version\" by ensuring all devices are up-to-date, as this indicates a communications protocol version mismatch. Note that yellow is Reactor PLC-specific, indicating duplicate unit IDs in use.")
|
||||
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the Supervisor.")
|
||||
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the Supervisor and this device do not match. Make sure everything is up-to-date.")
|
||||
sect("Versions")
|
||||
sect("Hardware Labels")
|
||||
doc("fp_fw", "FW", "Firmware application version of this device.")
|
||||
doc("fp_nt", "NT", "Network (comms) version this device has. These must match between devices in order for them to connect.")
|
||||
doc("fp_sn", "SN", "Device \"serial number\", made up of the Computer ID (shown on the Supervisor, Coordinator, and Pocket connection lists) and the device type.")
|
||||
|
||||
target = docs.fp.r_plc
|
||||
sect("Overview")
|
||||
@@ -515,12 +524,15 @@ doc("fp_crd_rtt", "RTT", "Each connection has a round trip time, or RTT. Since t
|
||||
list(DOC_LIST_TYPE.BULLET, { "green: <=1000ms", "yellow: <=1500ms ", "red: >1500ms" })
|
||||
sect("CRD Tab")
|
||||
text("This tab includes information about the Coordinator, partially covered by 'Common Items'.")
|
||||
doc("fp_crd_spkr", "SPEAKER", "This indicates if the speaker is connected.")
|
||||
doc("fp_crd_rt_main", "RT MAIN", "This indicates that the device's main loop co-routine is running.")
|
||||
doc("fp_crd_rt_render", "RT RENDER", "This indicates that the Coordinator graphics renderer co-routine is running.")
|
||||
doc("fp_crd_mon_main", "MAIN MONITOR", "The connection status of the main display monitor.")
|
||||
doc("fp_crd_mon_flow", "FLOW MONITOR", "The connection status of the coolant and waste flow display monitor.")
|
||||
doc("fp_crd_mon_unit", "UNIT X MONITOR", "The connection status of the monitor associated with a given unit.")
|
||||
doc("fp_crd_spkr", "SPEAKER", "This indicates if the speaker is connected.")
|
||||
doc("fp_crd_mon_main", "MAIN DISPLAY", "The status of the main display monitor.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "view unloaded", "view loaded" }, { colors.gray, colors.red, colors.green })
|
||||
doc("fp_crd_mon_flow", "FLOW DISPLAY", "The status of the coolant and waste flow display monitor.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "view unloaded", "view loaded" }, { colors.gray, colors.red, colors.green })
|
||||
doc("fp_crd_mon_unit", "UNIT X DISPLAY", "The status of the monitor associated with a given unit.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "view unloaded", "view loaded" }, { colors.gray, colors.red, colors.green })
|
||||
sect("API Tab")
|
||||
text("This tab lists connected pocket computers. Refer to the Supervisor PKT tab documentation for details on fields.")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local about_app = require("pocket.ui.apps.about")
|
||||
@@ -44,7 +44,7 @@ local APP_ID = pocket.APP_ID
|
||||
-- create new main view
|
||||
---@param main DisplayBox main displaybox
|
||||
local function init(main)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
-- window header message and connection status
|
||||
TextBox{parent=main,y=1,text=" S C ",fg_bg=style.header}
|
||||
@@ -54,13 +54,13 @@ local function init(main)
|
||||
db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
|
||||
db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
|
||||
|
||||
local start_pane = Div{parent=main,x=1,y=2}
|
||||
local main_pane = Div{parent=main,x=1,y=2}
|
||||
local start_pane = Div{parent=main,y=2}
|
||||
local main_pane = Div{parent=main,y=2}
|
||||
|
||||
WaitingAnim{parent=start_pane,x=12,y=7,fg_bg=cpair(colors.lightBlue,style.root.bkg)}
|
||||
TextBox{parent=start_pane,y=11,text="starting up...",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,style.root.bkg)}
|
||||
|
||||
local root_pane = MultiPane{parent=main,x=1,y=2,panes={start_pane,main_pane}}
|
||||
local root_pane = MultiPane{parent=main,y=2,panes={start_pane,main_pane}}
|
||||
|
||||
local page_div = Div{parent=main_pane,x=4,y=1}
|
||||
|
||||
@@ -81,10 +81,10 @@ local function init(main)
|
||||
-- verify all apps were created
|
||||
assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
|
||||
|
||||
db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()})
|
||||
db.nav.set_sidebar(Sidebar{parent=main_pane,x=1,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)})
|
||||
db.nav.set_pane(MultiPane{parent=page_div,y=1,panes=db.nav.get_containers()})
|
||||
db.nav.set_sidebar(Sidebar{parent=main_pane,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)})
|
||||
|
||||
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
|
||||
PushButton{parent=main_pane,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
|
||||
|
||||
db.nav.go_home()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -43,7 +43,7 @@ local mode_ind_s = {
|
||||
---@param ps psil
|
||||
---@param update function
|
||||
return function (app, page, panes, tank_pane, tank_id, ps, update)
|
||||
local fac = iocontrol.get_db().facility
|
||||
local fac = ioctl.get_db().facility
|
||||
|
||||
local tank_div = Div{parent=tank_pane,x=2,width=tank_pane.get_width()-2}
|
||||
table.insert(panes, tank_div)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Induction Matrix View
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -36,7 +36,7 @@ local wht_ind_s = style.icon_states.wht_ind_s
|
||||
---@param ps psil
|
||||
---@param update function
|
||||
return function (app, panes, matrix_pane, ps, update)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
local fac = db.facility
|
||||
|
||||
local mtx_div = Div{parent=matrix_pane,x=2,width=matrix_pane.get_width()-2}
|
||||
@@ -95,7 +95,7 @@ return function (app, panes, matrix_pane, ps, update)
|
||||
local chging = IconIndicator{parent=mtx_ext_div,y=3,label="Charging",states=wht_ind_s}
|
||||
local dischg = IconIndicator{parent=mtx_ext_div,y=4,label="Discharging",states=wht_ind_s}
|
||||
|
||||
TextBox{parent=mtx_ext_div,text="Energy Fill",x=1,y=6,width=13,fg_bg=label}
|
||||
TextBox{parent=mtx_ext_div,text="Energy Fill",y=6,width=13,fg_bg=label}
|
||||
local fill = DataIndicator{parent=mtx_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
|
||||
chging.register(ps, "is_charging", chging.update)
|
||||
@@ -104,18 +104,18 @@ return function (app, panes, matrix_pane, ps, update)
|
||||
|
||||
local max_io = IconIndicator{parent=mtx_ext_div,y=8,label="Max I/O Rate",states=yel_ind_s}
|
||||
|
||||
TextBox{parent=mtx_ext_div,text="Input Util.",x=1,y=10,width=13,fg_bg=label}
|
||||
TextBox{parent=mtx_ext_div,text="Input Util.",y=10,width=13,fg_bg=label}
|
||||
local in_util = DataIndicator{parent=mtx_ext_div,x=14,y=10,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
TextBox{parent=mtx_ext_div,text="Output Util.",x=1,y=11,width=13,fg_bg=label}
|
||||
TextBox{parent=mtx_ext_div,text="Output Util.",y=11,width=13,fg_bg=label}
|
||||
local out_util = DataIndicator{parent=mtx_ext_div,x=14,y=11,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
|
||||
max_io.register(ps, "at_max_io", max_io.update)
|
||||
in_util.register(ps, "last_input", function (x) in_util.update(calc_saturation(x) * 100) end)
|
||||
out_util.register(ps, "last_output", function (x) out_util.update(calc_saturation(x) * 100) end)
|
||||
|
||||
TextBox{parent=mtx_ext_div,text="Capacity ("..db.energy_label..")",x=1,y=13,fg_bg=label}
|
||||
TextBox{parent=mtx_ext_div,text="Capacity ("..db.energy_label..")",y=13,fg_bg=label}
|
||||
local capacity = DataIndicator{parent=mtx_ext_div,y=14,lu_colors=lu_col,label="",unit="",format="%21d",value=0,width=21,fg_bg=text_fg}
|
||||
TextBox{parent=mtx_ext_div,text="Max In/Out ("..db.energy_label.."/t)",x=1,y=15,fg_bg=label}
|
||||
TextBox{parent=mtx_ext_div,text="Max In/Out ("..db.energy_label.."/t)",y=15,fg_bg=label}
|
||||
local trans_cap = DataIndicator{parent=mtx_ext_div,y=16,lu_colors=lu_col,label="",unit="",format="%21d",rate=true,value=0,width=21,fg_bg=text_fg}
|
||||
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- SPS View
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -31,7 +31,7 @@ local text_fg = style.text_fg
|
||||
---@param ps psil
|
||||
---@param update function
|
||||
return function (app, panes, sps_pane, ps, update)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local sps_div = Div{parent=sps_pane,x=2,width=sps_pane.get_width()-2}
|
||||
table.insert(panes, sps_div)
|
||||
@@ -70,16 +70,16 @@ return function (app, panes, sps_pane, ps, update)
|
||||
|
||||
TextBox{parent=sps_ext_div,y=1,text="More SPS Info",alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=sps_ext_div,text="Polonium",x=1,y=3,width=13,fg_bg=label}
|
||||
TextBox{parent=sps_ext_div,text="Polonium",y=3,width=13,fg_bg=label}
|
||||
local input_p = DataIndicator{parent=sps_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local input_amnt = DataIndicator{parent=sps_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local input_amnt = DataIndicator{parent=sps_ext_div,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
input_p.register(ps, "input_fill", function (x) input_p.update(x * 100) end)
|
||||
input_amnt.register(ps, "input", function (x) input_amnt.update(x.amount) end)
|
||||
|
||||
TextBox{parent=sps_ext_div,text="Antimatter",x=1,y=6,width=15,fg_bg=label}
|
||||
TextBox{parent=sps_ext_div,text="Antimatter",y=6,width=15,fg_bg=label}
|
||||
local output_p = DataIndicator{parent=sps_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local output_amnt = DataIndicator{parent=sps_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="\xb5B",format="%18.3f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local output_amnt = DataIndicator{parent=sps_ext_div,y=7,lu_colors=lu_col,label="",unit="\xb5B",format="%18.3f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
output_p.register(ps, "output_fill", function (x) output_p.update(x * 100) end)
|
||||
output_amnt.register(ps, "output", function (x) output_amnt.update(x.amount) end)
|
||||
|
||||
@@ -46,8 +46,8 @@ return function (data, base_page, title, items, scroll_height)
|
||||
TextBox{parent=section_view_div,y=1,text=title,alignment=ALIGN.CENTER}
|
||||
PushButton{parent=section_view_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to}
|
||||
|
||||
local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=60,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local def_list = ListBox{parent=section_view_div,x=1,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local name_list = ListBox{parent=section_div,y=3,scroll_height=60,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
local def_list = ListBox{parent=section_view_div,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
|
||||
local sect_id = 1
|
||||
local page_end
|
||||
@@ -62,7 +62,7 @@ return function (data, base_page, title, items, scroll_height)
|
||||
local title_offs = string.len(title_text) + 2
|
||||
|
||||
local sect_title = Div{parent=def_list,height=1}
|
||||
TextBox{parent=sect_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=sect_title,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
local anchor = TextBox{parent=sect_title,x=title_offs,y=1,text=item.name,anchor=true,fg_bg=cpair(colors.green,colors.black)}
|
||||
|
||||
page_end = Div{parent=def_list,height=1,can_focus=true}
|
||||
@@ -80,7 +80,7 @@ return function (data, base_page, title, items, scroll_height)
|
||||
table.insert(search_db, { string.lower(item.name), item.name, title, view })
|
||||
|
||||
local name_title = Div{parent=name_list,height=1}
|
||||
TextBox{parent=name_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=name_title,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
PushButton{parent=name_title,x=title_offs,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.green,colors.black),active_fg_bg=btn_active,callback=view}
|
||||
|
||||
sect_id = sect_id + 1
|
||||
@@ -107,7 +107,7 @@ return function (data, base_page, title, items, scroll_height)
|
||||
table.insert(search_db, { string.lower(item.name), item.name, title, view })
|
||||
|
||||
local name_entry = Div{parent=name_list,height=#util.strwrap(item.name,name_list.get_width()-3)}
|
||||
TextBox{parent=name_entry,x=1,text="\x10",fg_bg=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=name_entry,text="\x10",fg_bg=cpair(colors.gray,colors.black)}
|
||||
PushButton{parent=name_entry,x=3,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
|
||||
elseif item.type == DOC_TYPE.TEXT then
|
||||
---@cast item pocket_doc_text
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Main Home Page
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -19,18 +19,18 @@ local APP_ID = pocket.APP_ID
|
||||
-- new home page view
|
||||
---@param root Container parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1,height=19}
|
||||
local main = Div{parent=root,y=1,height=19}
|
||||
|
||||
local app = db.nav.register_app(APP_ID.ROOT, main)
|
||||
|
||||
local apps_1 = Div{parent=main,x=1,y=1,height=15}
|
||||
local apps_2 = Div{parent=main,x=1,y=1,height=15}
|
||||
local apps_1 = Div{parent=main,y=1,height=15}
|
||||
local apps_2 = Div{parent=main,y=1,height=15}
|
||||
|
||||
local panes = { apps_1, apps_2 }
|
||||
|
||||
local app_pane = AppMultiPane{parent=main,x=1,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=app.switcher}
|
||||
local app_pane = AppMultiPane{parent=main,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=app.switcher}
|
||||
|
||||
app.set_root_pane(app_pane)
|
||||
app.new_page(app.new_page(nil, 1), 2)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -39,7 +39,7 @@ local yel_ind_s = style.icon_states.yel_ind_s
|
||||
---@param ps psil
|
||||
---@param update function
|
||||
return function (app, u_page, panes, blr_pane, b_id, ps, update)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local blr_div = Div{parent=blr_pane,x=2,width=blr_pane.get_width()-2}
|
||||
table.insert(panes, blr_div)
|
||||
@@ -51,12 +51,12 @@ return function (app, u_page, panes, blr_pane, b_id, ps, update)
|
||||
local status = StateIndicator{parent=blr_div,x=10,y=1,states=style.boiler.states,value=1,min_width=12}
|
||||
status.register(ps, "BoilerStateStatus", status.update)
|
||||
|
||||
local hcool = VerticalBar{parent=blr_div,x=1,y=4,fg_bg=cpair(colors.orange,colors.gray),height=5,width=1}
|
||||
local hcool = VerticalBar{parent=blr_div,y=4,fg_bg=cpair(colors.orange,colors.gray),height=5,width=1}
|
||||
local water = VerticalBar{parent=blr_div,x=3,y=4,fg_bg=cpair(colors.blue,colors.gray),height=5,width=1}
|
||||
local steam = VerticalBar{parent=blr_div,x=19,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
|
||||
local ccool = VerticalBar{parent=blr_div,x=21,y=4,fg_bg=cpair(colors.lightBlue,colors.gray),height=5,width=1}
|
||||
|
||||
TextBox{parent=blr_div,text="H",x=1,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=blr_div,text="H",y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=blr_div,text="W",x=3,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=blr_div,text="S",x=19,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=blr_div,text="C",x=21,y=3,width=1,fg_bg=label}
|
||||
@@ -78,7 +78,7 @@ return function (app, u_page, panes, blr_pane, b_id, ps, update)
|
||||
b_wll.register(ps, "WaterLevelLow", b_wll.update)
|
||||
b_hr.register(ps, "HeatingRateLow", b_hr.update)
|
||||
|
||||
TextBox{parent=blr_div,text="Boil Rate",x=1,y=13,width=12,fg_bg=label}
|
||||
TextBox{parent=blr_div,text="Boil Rate",y=13,width=12,fg_bg=label}
|
||||
local boil_r = DataIndicator{parent=blr_div,x=6,y=14,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=text_fg}
|
||||
|
||||
boil_r.register(ps, "boil_rate", boil_r.update)
|
||||
@@ -98,35 +98,35 @@ return function (app, u_page, panes, blr_pane, b_id, ps, update)
|
||||
return function (x) indicator.update(x.amount) end
|
||||
end
|
||||
|
||||
TextBox{parent=blr_ext_div,text="Hot Coolant",x=1,y=3,width=12,fg_bg=label}
|
||||
TextBox{parent=blr_ext_div,text="Hot Coolant",y=3,width=12,fg_bg=label}
|
||||
local heated_p = DataIndicator{parent=blr_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local hcool_amnt = DataIndicator{parent=blr_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local hcool_amnt = DataIndicator{parent=blr_ext_div,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
heated_p.register(ps, "hcool_fill", function (x) heated_p.update(x * 100) end)
|
||||
hcool_amnt.register(ps, "hcool", update_amount(hcool_amnt))
|
||||
|
||||
TextBox{parent=blr_ext_div,text="Water Tank",x=1,y=6,width=9,fg_bg=label}
|
||||
TextBox{parent=blr_ext_div,text="Water Tank",y=6,width=9,fg_bg=label}
|
||||
local fuel_p = DataIndicator{parent=blr_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local fuel_amnt = DataIndicator{parent=blr_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local fuel_amnt = DataIndicator{parent=blr_ext_div,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
fuel_p.register(ps, "water_fill", function (x) fuel_p.update(x * 100) end)
|
||||
fuel_amnt.register(ps, "water", update_amount(fuel_amnt))
|
||||
|
||||
TextBox{parent=blr_ext_div,text="Steam Tank",x=1,y=9,width=10,fg_bg=label}
|
||||
TextBox{parent=blr_ext_div,text="Steam Tank",y=9,width=10,fg_bg=label}
|
||||
local steam_p = DataIndicator{parent=blr_ext_div,x=14,y=9,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local steam_amnt = DataIndicator{parent=blr_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local steam_amnt = DataIndicator{parent=blr_ext_div,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
steam_p.register(ps, "steam_fill", function (x) steam_p.update(x * 100) end)
|
||||
steam_amnt.register(ps, "steam", update_amount(steam_amnt))
|
||||
|
||||
TextBox{parent=blr_ext_div,text="Cool Coolant",x=1,y=12,width=12,fg_bg=label}
|
||||
TextBox{parent=blr_ext_div,text="Cool Coolant",y=12,width=12,fg_bg=label}
|
||||
local cooled_p = DataIndicator{parent=blr_ext_div,x=14,y=12,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local ccool_amnt = DataIndicator{parent=blr_ext_div,x=1,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local ccool_amnt = DataIndicator{parent=blr_ext_div,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
cooled_p.register(ps, "ccool_fill", function (x) cooled_p.update(x * 100) end)
|
||||
ccool_amnt.register(ps, "ccool", update_amount(ccool_amnt))
|
||||
|
||||
TextBox{parent=blr_ext_div,text="Env. Loss",x=1,y=15,width=9,fg_bg=label}
|
||||
TextBox{parent=blr_ext_div,text="Env. Loss",y=15,width=9,fg_bg=label}
|
||||
local env_loss = DataIndicator{parent=blr_ext_div,x=11,y=15,lu_colors=lu_col,label="",unit="",format="%11.8f",value=0,width=11,fg_bg=text_fg}
|
||||
|
||||
env_loss.register(ps, "env_loss", env_loss.update)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -38,7 +38,7 @@ local yel_ind_s = style.icon_states.yel_ind_s
|
||||
---@param u_ps psil
|
||||
---@param update function
|
||||
return function (app, u_page, panes, page_div, u_ps, update)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local rct_pane = Div{parent=page_div}
|
||||
local rct_div = Div{parent=rct_pane,x=2,width=page_div.get_width()-2}
|
||||
@@ -51,12 +51,12 @@ return function (app, u_page, panes, page_div, u_ps, update)
|
||||
local status = StateIndicator{parent=rct_div,x=10,y=1,states=style.reactor.states,value=1,min_width=12}
|
||||
status.register(u_ps, "U_ReactorStateStatus", status.update)
|
||||
|
||||
local fuel = VerticalBar{parent=rct_div,x=1,y=4,fg_bg=cpair(colors.lightGray,colors.gray),height=5,width=1}
|
||||
local fuel = VerticalBar{parent=rct_div,y=4,fg_bg=cpair(colors.lightGray,colors.gray),height=5,width=1}
|
||||
local ccool = VerticalBar{parent=rct_div,x=3,y=4,fg_bg=cpair(colors.blue,colors.gray),height=5,width=1}
|
||||
local hcool = VerticalBar{parent=rct_div,x=19,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
|
||||
local waste = VerticalBar{parent=rct_div,x=21,y=4,fg_bg=cpair(colors.brown,colors.gray),height=5,width=1}
|
||||
|
||||
TextBox{parent=rct_div,text="F",x=1,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=rct_div,text="F",y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=rct_div,text="C",x=3,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=rct_div,text="H",x=19,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=rct_div,text="W",x=21,y=3,width=1,fg_bg=label}
|
||||
@@ -103,9 +103,9 @@ return function (app, u_page, panes, page_div, u_ps, update)
|
||||
r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
|
||||
r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
|
||||
|
||||
TextBox{parent=rct_div,text="HR",x=1,y=16,width=4,fg_bg=label}
|
||||
TextBox{parent=rct_div,text="HR",y=16,width=4,fg_bg=label}
|
||||
local heating_r = DataIndicator{parent=rct_div,x=6,y=16,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=text_fg}
|
||||
TextBox{parent=rct_div,text="DMG",x=1,y=17,width=4,fg_bg=label}
|
||||
TextBox{parent=rct_div,text="DMG",y=17,width=4,fg_bg=label}
|
||||
local damage_p = DataIndicator{parent=rct_div,x=6,y=17,lu_colors=lu_col,label="",unit="%",format="%11.2f",value=0,width=16,fg_bg=text_fg}
|
||||
|
||||
heating_r.register(u_ps, "heating_rate", heating_r.update)
|
||||
@@ -122,36 +122,36 @@ return function (app, u_page, panes, page_div, u_ps, update)
|
||||
|
||||
TextBox{parent=rct_ext_div,y=1,text="More Reactor Info",alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=rct_ext_div,text="Fuel Tank",x=1,y=3,width=9,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Fuel Tank",y=3,width=9,fg_bg=label}
|
||||
local fuel_p = DataIndicator{parent=rct_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local fuel_amnt = DataIndicator{parent=rct_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local fuel_amnt = DataIndicator{parent=rct_ext_div,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
fuel_p.register(u_ps, "fuel_fill", function (x) fuel_p.update(x * 100) end)
|
||||
fuel_amnt.register(u_ps, "fuel", fuel_amnt.update)
|
||||
|
||||
TextBox{parent=rct_ext_div,text="Cool Coolant",x=1,y=6,width=12,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Cool Coolant",y=6,width=12,fg_bg=label}
|
||||
local cooled_p = DataIndicator{parent=rct_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local ccool_amnt = DataIndicator{parent=rct_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local ccool_amnt = DataIndicator{parent=rct_ext_div,y=7,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
cooled_p.register(u_ps, "ccool_fill", function (x) cooled_p.update(x * 100) end)
|
||||
ccool_amnt.register(u_ps, "ccool_amnt", ccool_amnt.update)
|
||||
|
||||
TextBox{parent=rct_ext_div,text="Hot Coolant",x=1,y=9,width=12,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Hot Coolant",y=9,width=12,fg_bg=label}
|
||||
local heated_p = DataIndicator{parent=rct_ext_div,x=14,y=9,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local hcool_amnt = DataIndicator{parent=rct_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local hcool_amnt = DataIndicator{parent=rct_ext_div,y=10,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
heated_p.register(u_ps, "hcool_fill", function (x) heated_p.update(x * 100) end)
|
||||
hcool_amnt.register(u_ps, "hcool_amnt", hcool_amnt.update)
|
||||
|
||||
TextBox{parent=rct_ext_div,text="Waste Tank",x=1,y=12,width=10,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Waste Tank",y=12,width=10,fg_bg=label}
|
||||
local waste_p = DataIndicator{parent=rct_ext_div,x=14,y=12,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local waste_amnt = DataIndicator{parent=rct_ext_div,x=1,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local waste_amnt = DataIndicator{parent=rct_ext_div,y=13,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
waste_p.register(u_ps, "waste_fill", function (x) waste_p.update(x * 100) end)
|
||||
waste_amnt.register(u_ps, "waste", waste_amnt.update)
|
||||
|
||||
TextBox{parent=rct_ext_div,text="Boil Eff.",x=1,y=15,width=9,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Env. Loss",x=1,y=16,width=9,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Boil Eff.",y=15,width=9,fg_bg=label}
|
||||
TextBox{parent=rct_ext_div,text="Env. Loss",y=16,width=9,fg_bg=label}
|
||||
local boil_eff = DataIndicator{parent=rct_ext_div,x=11,y=15,lu_colors=lu_col,label="",unit="%",format="%9.2f",value=0,width=11,fg_bg=text_fg}
|
||||
local env_loss = DataIndicator{parent=rct_ext_div,x=11,y=16,lu_colors=lu_col,label="",unit="",format="%11.8f",value=0,width=11,fg_bg=text_fg}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local ioctl = require("pocket.ioctl")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
@@ -41,7 +41,7 @@ local yel_ind_s = style.icon_states.yel_ind_s
|
||||
---@param ps psil
|
||||
---@param update function
|
||||
return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update)
|
||||
local db = iocontrol.get_db()
|
||||
local db = ioctl.get_db()
|
||||
|
||||
local tbn_div = Div{parent=tbn_pane,x=2,width=tbn_pane.get_width()-2}
|
||||
table.insert(panes, tbn_div)
|
||||
@@ -53,10 +53,10 @@ return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update)
|
||||
local status = StateIndicator{parent=tbn_div,x=10,y=1,states=style.turbine.states,value=1,min_width=12}
|
||||
status.register(ps, "TurbineStateStatus", status.update)
|
||||
|
||||
local steam = VerticalBar{parent=tbn_div,x=1,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
|
||||
local steam = VerticalBar{parent=tbn_div,y=4,fg_bg=cpair(colors.white,colors.gray),height=5,width=1}
|
||||
local ccool = VerticalBar{parent=tbn_div,x=21,y=4,fg_bg=cpair(colors.green,colors.gray),height=5,width=1}
|
||||
|
||||
TextBox{parent=tbn_div,text="S",x=1,y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=tbn_div,text="S",y=3,width=1,fg_bg=label}
|
||||
TextBox{parent=tbn_div,text="E",x=21,y=3,width=1,fg_bg=label}
|
||||
|
||||
steam.register(ps, "steam_fill", steam.update)
|
||||
@@ -94,22 +94,22 @@ return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update)
|
||||
|
||||
TextBox{parent=tbn_ext_div,y=1,text="More Turbine Info",alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=tbn_ext_div,text="Steam Tank",x=1,y=3,width=10,fg_bg=label}
|
||||
TextBox{parent=tbn_ext_div,text="Steam Tank",y=3,width=10,fg_bg=label}
|
||||
local steam_p = DataIndicator{parent=tbn_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local steam_amnt = DataIndicator{parent=tbn_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
local steam_amnt = DataIndicator{parent=tbn_ext_div,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
|
||||
|
||||
steam_p.register(ps, "steam_fill", function (x) steam_p.update(x * 100) end)
|
||||
steam_amnt.register(ps, "steam", function (x) steam_amnt.update(x.amount) end)
|
||||
|
||||
TextBox{parent=tbn_ext_div,text="Energy Fill",x=1,y=6,width=12,fg_bg=label}
|
||||
TextBox{parent=tbn_ext_div,text="Energy Fill",y=6,width=12,fg_bg=label}
|
||||
local charge_p = DataIndicator{parent=tbn_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
|
||||
local charge_amnt = PowerIndicator{parent=tbn_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit=db.energy_label,format="%17.4f",value=0,width=21,fg_bg=text_fg}
|
||||
local charge_amnt = PowerIndicator{parent=tbn_ext_div,y=7,lu_colors=lu_col,label="",unit=db.energy_label,format="%17.4f",value=0,width=21,fg_bg=text_fg}
|
||||
|
||||
charge_p.register(ps, "energy_fill", function (x) charge_p.update(x * 100) end)
|
||||
charge_amnt.register(ps, "energy", function (val) charge_amnt.update(db.energy_convert(val)) end)
|
||||
|
||||
TextBox{parent=tbn_ext_div,text="Rotation Rate",x=1,y=9,width=13,fg_bg=label}
|
||||
local rotation = DataIndicator{parent=tbn_ext_div,x=1,y=10,lu_colors=lu_col,label="",unit="",format="%21.12f",value=0,width=21,fg_bg=text_fg}
|
||||
TextBox{parent=tbn_ext_div,text="Rotation Rate",y=9,width=13,fg_bg=label}
|
||||
local rotation = DataIndicator{parent=tbn_ext_div,y=10,lu_colors=lu_col,label="",unit="",format="%21.12f",value=0,width=21,fg_bg=text_fg}
|
||||
|
||||
rotation.register(ps, "steam", function ()
|
||||
local ok, result = pcall(function () return util.turbine_rotation(db.units[u_id].turbine_data_tbl[t_id]) end)
|
||||
|
||||
383
reactor-plc/backplane.lua
Normal file
383
reactor-plc/backplane.lua
Normal file
@@ -0,0 +1,383 @@
|
||||
--
|
||||
-- Reactor PLC System Core Peripheral Backplane
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
|
||||
local println = util.println
|
||||
|
||||
---@class plc_backplane
|
||||
local backplane = {}
|
||||
|
||||
local _bp = {
|
||||
smem = nil, ---@type plc_shared_memory
|
||||
|
||||
wlan_pref = true,
|
||||
lan_iface = "",
|
||||
|
||||
act_nic = nil, ---@type nic
|
||||
wd_nic = nil, ---@type nic|nil
|
||||
wl_nic = nil, ---@type nic|nil
|
||||
nic_map = {} ---@type nic[] connected nics
|
||||
}
|
||||
|
||||
local multi_reactor_warn = "BKPLN: do NOT share reactor connections between multiple PLCs! they may not all be protected and used as configured"
|
||||
|
||||
-- network interfaces indexed by peripheral names
|
||||
backplane.nics = _bp.nic_map
|
||||
|
||||
-- initialize the system peripheral backplane<br>
|
||||
---@param config plc_config
|
||||
---@param __shared_memory plc_shared_memory
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function backplane.init(config, __shared_memory)
|
||||
_bp.smem = __shared_memory
|
||||
_bp.wlan_pref = config.PreferWireless
|
||||
_bp.lan_iface = config.WiredModem
|
||||
|
||||
local plc_dev = __shared_memory.plc_dev
|
||||
local plc_state = __shared_memory.plc_state
|
||||
|
||||
plc_state.degraded = false
|
||||
|
||||
-- Modem Init
|
||||
|
||||
if _bp.smem.networked then
|
||||
-- init wired NIC
|
||||
if type(_bp.lan_iface) == "string" then
|
||||
local modem = ppm.get_modem(_bp.lan_iface)
|
||||
local wd_nic = network.nic(modem, config.SVR_Channel)
|
||||
|
||||
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
|
||||
|
||||
_bp.wd_nic = wd_nic
|
||||
_bp.act_nic = wd_nic -- set this as active for now
|
||||
_bp.nic_map[_bp.lan_iface] = wd_nic
|
||||
|
||||
wd_nic.closeAll()
|
||||
wd_nic.open(config.PLC_Channel)
|
||||
|
||||
plc_state.wd_modem = wd_nic.is_connected()
|
||||
end
|
||||
|
||||
-- init wireless NIC(s)
|
||||
if config.WirelessModem then
|
||||
local modem, iface = ppm.get_wireless_modem()
|
||||
local wl_nic = network.nic(modem, config.SVR_Channel)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
|
||||
|
||||
-- set this as active if connected or if both modems are disconnected and this is preferred
|
||||
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
|
||||
_bp.act_nic = wl_nic
|
||||
log.info("BKPLN: switched active to preferred wireless")
|
||||
end
|
||||
|
||||
_bp.wl_nic = wl_nic
|
||||
if iface then _bp.nic_map[iface] = wl_nic end
|
||||
|
||||
wl_nic.closeAll()
|
||||
wl_nic.open(config.PLC_Channel)
|
||||
|
||||
plc_state.wl_modem = wl_nic.is_connected()
|
||||
end
|
||||
|
||||
-- comms modem is required if networked
|
||||
if not (plc_state.wd_modem or plc_state.wl_modem) then
|
||||
println("startup> no comms modem found")
|
||||
log.warning("BKPLN: no comms modem on startup")
|
||||
|
||||
plc_state.degraded = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Reactor Init
|
||||
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
plc_dev.reactor = ppm.get_fission_reactor()
|
||||
plc_state.no_reactor = plc_dev.reactor == nil
|
||||
|
||||
-- we need a reactor, can at least do some things even if it isn't formed though
|
||||
if plc_state.no_reactor then
|
||||
log.info("BKPLN: REACTOR LINK_DOWN")
|
||||
|
||||
println("startup> fission reactor not found")
|
||||
log.warning("BKPLN: no reactor on startup")
|
||||
|
||||
plc_state.degraded = true
|
||||
plc_state.reactor_formed = false
|
||||
|
||||
-- mount a virtual peripheral to init the RPS with
|
||||
local _, dev = ppm.mount_virtual()
|
||||
plc_dev.reactor = dev
|
||||
|
||||
log.info("BKPLN: mounted virtual device as reactor")
|
||||
else
|
||||
log.info("BKPLN: REACTOR LINK_UP " .. ppm.get_iface(plc_dev.reactor))
|
||||
|
||||
if not plc_dev.reactor.isFormed() then
|
||||
println("startup> fission reactor is not formed")
|
||||
log.warning("BKPLN: reactor logic adapter detected, but reactor is not formed")
|
||||
|
||||
plc_state.degraded = true
|
||||
plc_state.reactor_formed = false
|
||||
end
|
||||
end
|
||||
|
||||
-- detect and warn about multiple reactors
|
||||
if #ppm.get_all_devices("fissionReactorLogicAdapter") > 1 then
|
||||
println("startup> !! DANGER !! more than one reactor was detected! do not share reactor connections between multiple PLCs! they may not all be protected and used as configured")
|
||||
|
||||
log.warning("BKPLN: !! DANGER !! more than one reactor was detected on startup!")
|
||||
log.warning(multi_reactor_warn)
|
||||
|
||||
databus.tx_multi_reactor(true)
|
||||
end
|
||||
end
|
||||
|
||||
-- get the active NIC
|
||||
function backplane.active_nic() return _bp.act_nic end
|
||||
|
||||
-- get the standby NIC
|
||||
---@return nic|nil
|
||||
function backplane.standby_nic() return util.trinary(_bp.act_nic == _bp.wl_nic, _bp.wd_nic, _bp.wl_nic) end
|
||||
|
||||
-- periodic backplane peripheral tasks
|
||||
function backplane.periodic()
|
||||
if _bp.wd_nic then databus.tx_wd_net(_bp.wd_nic.periodic()) end
|
||||
if _bp.wl_nic then databus.tx_wl_net(_bp.wl_nic.periodic()) end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral attach
|
||||
---@param iface string
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param print_no_fp function
|
||||
function backplane.attach(iface, type, device, print_no_fp)
|
||||
local MQ__RPS_CMD = _bp.smem.q_types.MQ__RPS_CMD
|
||||
|
||||
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
|
||||
|
||||
local networked = _bp.smem.networked
|
||||
local state = _bp.smem.plc_state
|
||||
local dev = _bp.smem.plc_dev
|
||||
local sys = _bp.smem.plc_sys
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "fissionReactorLogicAdapter" then
|
||||
if not state.no_reactor then
|
||||
log.warning("BKPLN: !! DANGER !! an additional reactor (" .. iface .. ") was connected and will not be used!")
|
||||
log.warning(multi_reactor_warn)
|
||||
|
||||
databus.tx_multi_reactor(true)
|
||||
return
|
||||
end
|
||||
|
||||
-- reconnected reactor
|
||||
log.info("BKPLN: REACTOR LINK_UP " .. iface)
|
||||
|
||||
dev.reactor = device
|
||||
state.no_reactor = false
|
||||
|
||||
print_no_fp("reactor connected")
|
||||
log.info("BKPLN: reactor connected")
|
||||
|
||||
-- we need to assume formed here as we cannot check in this main loop
|
||||
-- RPS will identify if it isn't and this will get set false later
|
||||
state.reactor_formed = true
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if ((not networked) or (state.wd_modem or state.wl_modem)) and state.reactor_formed then
|
||||
state.degraded = false
|
||||
end
|
||||
|
||||
sys.rps.reconnect_reactor(dev.reactor)
|
||||
if networked then
|
||||
sys.plc_comms.reconnect_reactor(dev.reactor)
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed/reconnected
|
||||
-- without this, auto control can't resume on chunk load
|
||||
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.RESET_REATTACH)
|
||||
elseif networked and type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
local m_is_wl = device.isWireless()
|
||||
|
||||
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
|
||||
|
||||
if wd_nic and (_bp.lan_iface == iface) then
|
||||
-- connect this as the wired NIC
|
||||
wd_nic.connect(device)
|
||||
_bp.nic_map[iface] = wd_nic
|
||||
|
||||
log.info("BKPLN: WIRED PHY_UP " .. iface)
|
||||
print_no_fp("wired comms modem connected")
|
||||
|
||||
state.wd_modem = true
|
||||
|
||||
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
|
||||
-- switch back to preferred wired
|
||||
_bp.act_nic = wd_nic
|
||||
|
||||
sys.plc_comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wired modem (preferred)")
|
||||
end
|
||||
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
|
||||
-- connect this as the wireless NIC
|
||||
wl_nic.connect(device)
|
||||
_bp.nic_map[iface] = wl_nic
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
|
||||
print_no_fp("wireless comms modem connected")
|
||||
|
||||
state.wl_modem = true
|
||||
|
||||
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
|
||||
-- switch back to preferred wireless
|
||||
_bp.act_nic = wl_nic
|
||||
|
||||
sys.plc_comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wireless modem (preferred)")
|
||||
end
|
||||
elseif wl_nic and m_is_wl then
|
||||
-- the wireless NIC already has a modem
|
||||
device.closeAll()
|
||||
|
||||
print_no_fp("standby wireless modem connected")
|
||||
log.info("BKPLN: standby wireless modem connected")
|
||||
else
|
||||
device.closeAll()
|
||||
|
||||
print_no_fp("unassigned modem connected")
|
||||
log.warning("BKPLN: unassigned modem connected")
|
||||
end
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (state.wd_modem or state.wl_modem) and state.reactor_formed and not state.no_reactor then
|
||||
state.degraded = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral detach
|
||||
---@param iface string
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param print_no_fp function
|
||||
function backplane.detach(iface, type, device, print_no_fp)
|
||||
local MQ__RPS_CMD = _bp.smem.q_types.MQ__RPS_CMD
|
||||
|
||||
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
|
||||
|
||||
local state = _bp.smem.plc_state
|
||||
local dev = _bp.smem.plc_dev
|
||||
local sys = _bp.smem.plc_sys
|
||||
|
||||
if type == "fissionReactorLogicAdapter" then
|
||||
log.info("BKPLN: REACTOR LINK_DOWN " .. iface)
|
||||
|
||||
-- detect and warn about multiple reactors
|
||||
if #ppm.get_all_devices("fissionReactorLogicAdapter") > 1 then
|
||||
log.warning("BKPLN: !! DANGER !! more than one reactor is still present!")
|
||||
log.warning(multi_reactor_warn)
|
||||
|
||||
databus.tx_multi_reactor(true)
|
||||
else databus.tx_multi_reactor(false) end
|
||||
|
||||
-- if this is the active reactor, handle that
|
||||
if device == dev.reactor then
|
||||
print_no_fp("reactor disconnected")
|
||||
log.warning("BKPLN: reactor disconnected")
|
||||
|
||||
state.no_reactor = true
|
||||
state.degraded = true
|
||||
|
||||
-- try to find another reactor (this should not work unless multiple were incorrectly connected)
|
||||
local reactor, r_iface = ppm.get_fission_reactor()
|
||||
if reactor and r_iface then
|
||||
log.info("BKPLN: found another fission reactor logic adapter")
|
||||
|
||||
backplane.attach(r_iface, type, reactor, print_no_fp)
|
||||
end
|
||||
end
|
||||
elseif _bp.smem.networked and type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
log.info(util.c("BKPLN: PHY_DETACH ", iface))
|
||||
|
||||
_bp.nic_map[iface] = nil
|
||||
|
||||
if wd_nic and wd_nic.is_modem(device) then
|
||||
wd_nic.disconnect()
|
||||
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
|
||||
|
||||
state.wd_modem = false
|
||||
elseif wl_nic and wl_nic.is_modem(device) then
|
||||
wl_nic.disconnect()
|
||||
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
|
||||
|
||||
state.wl_modem = false
|
||||
end
|
||||
|
||||
-- we only care if this is our active comms modem
|
||||
if _bp.act_nic.is_modem(device) then
|
||||
print_no_fp("active comms modem disconnected")
|
||||
log.warning("BKPLN: active comms modem disconnected")
|
||||
|
||||
-- failover and try to find a new comms modem
|
||||
if _bp.act_nic == wl_nic then
|
||||
-- wireless active disconnected
|
||||
-- try to find another wireless modem, otherwise switch to wired
|
||||
local modem, m_iface = ppm.get_wireless_modem()
|
||||
if wl_nic and modem then
|
||||
log.info("BKPLN: found another wireless modem, using it for comms")
|
||||
|
||||
wl_nic.connect(modem)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
|
||||
|
||||
state.wl_modem = true
|
||||
elseif wd_nic and wd_nic.is_connected() then
|
||||
_bp.act_nic = wd_nic
|
||||
|
||||
sys.plc_comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wired modem")
|
||||
else
|
||||
-- no other wireless modems, wired unavailable
|
||||
state.degraded = true
|
||||
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
end
|
||||
elseif wl_nic and wl_nic.is_connected() then
|
||||
-- wired active disconnected, wireless available
|
||||
_bp.act_nic = wl_nic
|
||||
|
||||
sys.plc_comms.switch_nic(_bp.act_nic)
|
||||
log.info("BKPLN: switched comms to wireless modem")
|
||||
else
|
||||
-- wired active disconnected, wireless unavailable
|
||||
state.degraded = true
|
||||
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
end
|
||||
elseif wd_nic and wd_nic.is_modem(device) then
|
||||
-- wired, but not active
|
||||
print_no_fp("standby wired modem disconnected")
|
||||
log.info("BKPLN: standby wired modem disconnected")
|
||||
elseif wl_nic and wl_nic.is_modem(device) then
|
||||
-- wireless, but not active
|
||||
print_no_fp("standby wireless modem disconnected")
|
||||
log.info("BKPLN: standby wireless modem disconnected")
|
||||
else
|
||||
print_no_fp("unassigned modem disconnected")
|
||||
log.warning("BKPLN: unassigned modem disconnected")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return backplane
|
||||
@@ -24,10 +24,12 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
local self = {
|
||||
checking_wl = true,
|
||||
wd_modem = nil, ---@type Modem|nil
|
||||
wl_modem = nil, ---@type Modem|nil
|
||||
|
||||
nic = nil, ---@type nic
|
||||
net_listen = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = util.time_ms() * 10,
|
||||
|
||||
self_check_pass = true,
|
||||
|
||||
@@ -48,22 +50,20 @@ local function check_complete()
|
||||
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
|
||||
end
|
||||
|
||||
-- send a management packet to the supervisor
|
||||
-- send a management packet to the supervisor (one-time broadcast)
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function send_sv(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(comms.BROADCAST, util.time_ms() * 10, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
self.nic.transmit(self.settings.SVR_Channel, self.settings.PLC_Channel, s_pkt)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
self.nic.transmit(self.settings.SVR_Channel, self.settings.PLC_Channel, frame)
|
||||
end
|
||||
|
||||
-- handle an establish message from the supervisor
|
||||
---@param packet mgmt_frame
|
||||
---@param packet mgmt_packet
|
||||
local function handle_packet(packet)
|
||||
local error_msg = nil
|
||||
|
||||
@@ -75,10 +75,7 @@ local function handle_packet(packet)
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack== ESTABLISH_ACK.ALLOW then
|
||||
self.self_check_msg(nil, true, "")
|
||||
self.sv_addr = packet.scada_frame.src_addr()
|
||||
send_sv(MGMT_TYPE.CLOSE, {})
|
||||
if self.self_check_pass then check_complete() end
|
||||
-- success
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
error_msg = "error: supervisor connection denied"
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
@@ -97,18 +94,20 @@ local function handle_packet(packet)
|
||||
end
|
||||
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
|
||||
if error_msg then
|
||||
self.self_check_msg(nil, false, error_msg)
|
||||
else
|
||||
self.self_check_msg(nil, true, "")
|
||||
end
|
||||
|
||||
util.push_event("conn_test_complete", error_msg == nil)
|
||||
end
|
||||
|
||||
-- handle supervisor connection failure
|
||||
local function handle_timeout()
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
|
||||
util.push_event("conn_test_complete", false)
|
||||
end
|
||||
|
||||
-- execute the self-check
|
||||
@@ -121,42 +120,61 @@ local function self_check()
|
||||
self.self_check_pass = true
|
||||
|
||||
local cfg = self.settings
|
||||
local modem = ppm.get_wireless_modem()
|
||||
self.wd_modem = ppm.get_modem(cfg.WiredModem)
|
||||
self.wl_modem = ppm.get_wireless_modem()
|
||||
local reactor = ppm.get_fission_reactor()
|
||||
local valid_cfg = plc.validate_config(cfg)
|
||||
|
||||
-- check for comms modems
|
||||
if cfg.Networked then
|
||||
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
|
||||
if cfg.WiredModem then
|
||||
self.self_check_msg("> check wired comms modem connected...", self.wd_modem, "please connect the wired comms modem " .. cfg.WiredModem)
|
||||
end
|
||||
|
||||
if cfg.WirelessModem then
|
||||
self.self_check_msg("> check wireless/ender modem connected...", self.wl_modem, "please connect an ender or wireless modem for wireless comms")
|
||||
end
|
||||
end
|
||||
|
||||
self.self_check_msg("> check fission reactor connected...", reactor ~= nil, "please connect the reactor PLC to the reactor's fission reactor logic adapter")
|
||||
self.self_check_msg("> check fission reactor formed...")
|
||||
-- this consumes events, but that is fine here
|
||||
self.self_check_msg(nil, reactor and reactor.isFormed(), "ensure the fission reactor multiblock is formed")
|
||||
self.self_check_msg("> check for no more than one reactor...", #ppm.get_all_devices("fissionReactorLogicAdapter") <= 1, "there MUST be no more than one reactor connected, as a PLC uses the first one it finds, which may not always be the same one")
|
||||
|
||||
self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones")
|
||||
|
||||
if cfg.Networked and valid_cfg and modem then
|
||||
self.self_check_msg("> check supervisor connection...")
|
||||
if cfg.Networked and valid_cfg then
|
||||
self.checking_wl = true
|
||||
|
||||
-- init mac as needed
|
||||
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
|
||||
network.init_mac(cfg.AuthKey)
|
||||
if cfg.WirelessModem and self.wl_modem then
|
||||
self.self_check_msg("> check wireless supervisor connection...")
|
||||
|
||||
-- init mac as needed
|
||||
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
|
||||
network.init_mac(cfg.AuthKey)
|
||||
else
|
||||
network.deinit_mac()
|
||||
end
|
||||
|
||||
comms.set_trusted_range(cfg.TrustedRange)
|
||||
|
||||
self.nic = network.nic(self.wl_modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(cfg.PLC_Channel)
|
||||
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.PLC, cfg.UnitID })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
elseif cfg.WiredModem and self.wd_modem then
|
||||
-- skip to wired
|
||||
util.push_event("conn_test_complete", true)
|
||||
else
|
||||
network.deinit_mac()
|
||||
self.self_check_msg("> no modem, can't test supervisor connection", false)
|
||||
end
|
||||
|
||||
self.nic = network.nic(modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(cfg.PLC_Channel)
|
||||
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, cfg.UnitID })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
else
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
@@ -191,9 +209,9 @@ function check.create(main_pane, settings_cfg, check_sys, style)
|
||||
|
||||
local sc = Div{parent=check_sys,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=check_sys,x=1,y=2,text=" Reactor PLC Self-Check",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=check_sys,y=2,text=" Reactor PLC Self-Check",fg_bg=bw_fg_bg}
|
||||
|
||||
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
self.sc_log = ListBox{parent=sc,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local last_check = { nil, nil }
|
||||
|
||||
@@ -216,7 +234,7 @@ function check.create(main_pane, settings_cfg, check_sys, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sc,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
end
|
||||
|
||||
@@ -228,16 +246,56 @@ end
|
||||
---@param distance integer
|
||||
function check.receive_sv(side, sender, reply_to, message, distance)
|
||||
if self.nic ~= nil and self.net_listen then
|
||||
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
local frame = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
if frame and frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local pkt = comms.mgmt_container().decode(frame)
|
||||
if pkt then
|
||||
tcd.abort(handle_timeout)
|
||||
handle_packet(mgmt_pkt.get())
|
||||
handle_packet(pkt)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle completed connection tests
|
||||
---@param pass boolean
|
||||
function check.conn_test_callback(pass)
|
||||
local cfg = self.settings
|
||||
|
||||
if self.checking_wl then
|
||||
if not pass then
|
||||
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wireless interface, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
|
||||
end
|
||||
|
||||
if cfg.WiredModem and self.wd_modem then
|
||||
self.checking_wl = false
|
||||
self.self_check_msg("> check wired supervisor connection...")
|
||||
|
||||
comms.set_trusted_range(0)
|
||||
|
||||
self.nic = network.nic(self.wd_modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(cfg.PLC_Channel)
|
||||
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.PLC, cfg.UnitID })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
else
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
end
|
||||
else
|
||||
if not pass then
|
||||
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wired interface, the wire is intact, and your channels are correct")
|
||||
end
|
||||
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
end
|
||||
end
|
||||
|
||||
return check
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
@@ -20,6 +21,8 @@ local TextField = require("graphics.elements.form.TextField")
|
||||
|
||||
local IndLight = require("graphics.elements.indicators.IndicatorLight")
|
||||
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
@@ -30,6 +33,10 @@ local self = {
|
||||
set_networked = nil, ---@type function
|
||||
bundled_emcool = nil, ---@type function
|
||||
|
||||
wireless = nil, ---@type Checkbox
|
||||
wl_pref = nil, ---@type Checkbox
|
||||
wired = nil, ---@type Checkbox
|
||||
range = nil, ---@type NumberField
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
@@ -84,27 +91,27 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_5 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
|
||||
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4,plc_c_5}}
|
||||
local plc_pane = MultiPane{parent=plc_cfg,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4,plc_c_5}}
|
||||
|
||||
TextBox{parent=plc_cfg,x=1,y=2,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
|
||||
TextBox{parent=plc_cfg,y=2,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
|
||||
|
||||
TextBox{parent=plc_c_1,x=1,y=1,text="Would you like to set this PLC as networked?"}
|
||||
TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=plc_c_1,y=1,text="Would you like to set this PLC as networked?"}
|
||||
TextBox{parent=plc_c_1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local networked = Checkbox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
local networked = Checkbox{parent=plc_c_1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
|
||||
local function submit_networked()
|
||||
self.set_networked(networked.get_value())
|
||||
plc_pane.set_value(2)
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_1,x=44,y=14,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_2,x=1,y=1,text="Please enter the reactor unit ID for this PLC."}
|
||||
TextBox{parent=plc_c_2,x=1,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=plc_c_2,y=1,text="Please enter the reactor unit ID for this PLC."}
|
||||
TextBox{parent=plc_c_2,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_2,x=1,y=6,text="Unit #"}
|
||||
TextBox{parent=plc_c_2,y=6,text="Unit #"}
|
||||
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_chars=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
@@ -123,13 +130,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
else u_id_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_2,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_2,y=14,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_2,x=44,y=14,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC."}
|
||||
TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=plc_c_3,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC."}
|
||||
TextBox{parent=plc_c_3,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local en_em_cool = Checkbox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
local en_em_cool = Checkbox{parent=plc_c_3,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
|
||||
local function next_from_plc()
|
||||
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end
|
||||
@@ -140,33 +147,32 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_3,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_3,y=14,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=plc_c_4,x=1,y=1,text="Emergency Coolant Redstone Output Side"}
|
||||
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
|
||||
TextBox{parent=plc_c_4,y=1,text="Emergency Coolant Redstone Output Side"}
|
||||
local side = Radio2D{parent=plc_c_4,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
|
||||
|
||||
TextBox{parent=plc_c_4,x=1,y=5,text="Bundled Redstone Configuration"}
|
||||
local bundled = Checkbox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)self.bundled_emcool(v)end}
|
||||
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=plc_c_4,y=5,text="Bundled Redstone Configuration"}
|
||||
local bundled = Checkbox{parent=plc_c_4,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)self.bundled_emcool(v)end}
|
||||
local color = Radio2D{parent=plc_c_4,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
if ini_cfg.EmerCoolColor == nil then color.disable() end
|
||||
|
||||
function self.bundled_emcool(en) if en then color.enable() else color.disable() end end
|
||||
|
||||
TextBox{parent=plc_c_5,x=1,y=1,height=5,text="Advanced Options"}
|
||||
local invert = Checkbox{parent=plc_c_5,x=1,y=3,label="Invert",default=ini_cfg.EmerCoolInvert,box_fg_bg=cpair(colors.orange,colors.black),callback=function()end}
|
||||
TextBox{parent=plc_c_5,x=10,y=3,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
TextBox{parent=plc_c_5,y=1,height=5,text="Advanced Options"}
|
||||
local invert = Checkbox{parent=plc_c_5,y=3,label="Invert",default=ini_cfg.EmerCoolInvert,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||
TextBox{parent=plc_c_5,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
PushButton{parent=plc_c_5,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_5,y=14,text="\x1b Back",callback=function()plc_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local function submit_emcool()
|
||||
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
|
||||
tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
|
||||
tmp_cfg.EmerCoolColor = tri(bundled.get_value(), color_options_map[color.get_value()], nil)
|
||||
tmp_cfg.EmerCoolInvert = invert.get_value()
|
||||
next_from_plc()
|
||||
end
|
||||
|
||||
PushButton{parent=plc_c_4,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_4,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=plc_c_4,x=33,y=14,min_width=10,text="Advanced",callback=function()plc_pane.set_value(5)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=plc_c_4,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
@@ -177,22 +183,88 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
||||
local net_pane = MultiPane{parent=net_cfg,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
TextBox{parent=net_cfg,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,y=1,text="Please select the network interface(s)."}
|
||||
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,x=1,y=11,text="PLC Channel"}
|
||||
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local function en_dis_pref()
|
||||
if self.wireless.get_value() and self.wired.get_value() then
|
||||
self.wl_pref.enable()
|
||||
else
|
||||
self.wl_pref.set_value(self.wireless.get_value())
|
||||
self.wl_pref.disable()
|
||||
end
|
||||
end
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local function on_wired_change(_)
|
||||
en_dis_pref()
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
self.wireless = Checkbox{parent=net_c_1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=en_dis_pref}
|
||||
self.wl_pref = Checkbox{parent=net_c_1,x=30,y=3,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
self.wired = Checkbox{parent=net_c_1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
|
||||
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
|
||||
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
|
||||
local modem_list = ListBox{parent=net_c_1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
en_dis_pref()
|
||||
|
||||
local function submit_interfaces()
|
||||
tmp_cfg.WirelessModem = self.wireless.get_value()
|
||||
|
||||
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
|
||||
tmp_cfg.PreferWireless = self.wl_pref.get_value()
|
||||
else
|
||||
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
|
||||
self.wl_pref.set_value(tmp_cfg.PreferWireless)
|
||||
end
|
||||
|
||||
if not self.wired.get_value() then
|
||||
tmp_cfg.WiredModem = false
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
if not (self.wired.get_value() or self.wireless.get_value()) then
|
||||
modem_err.set_value("Please select a modem type.")
|
||||
modem_err.show()
|
||||
elseif self.wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
|
||||
modem_err.set_value("Please select a wired modem.")
|
||||
modem_err.show()
|
||||
else
|
||||
if tmp_cfg.WirelessModem then
|
||||
self.range.enable()
|
||||
else
|
||||
self.range.set_value(0)
|
||||
self.range.disable()
|
||||
end
|
||||
|
||||
net_pane.set_value(2)
|
||||
modem_err.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_2,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,y=8,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_2,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,y=11,text="PLC Channel"}
|
||||
local plc_chan = NumberField{parent=net_c_2,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c = tonumber(svr_chan.get_value())
|
||||
@@ -200,7 +272,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
if svr_c ~= nil and plc_c ~= nil then
|
||||
tmp_cfg.SVR_Channel = svr_c
|
||||
tmp_cfg.PLC_Channel = plc_c
|
||||
net_pane.set_value(2)
|
||||
net_pane.set_value(3)
|
||||
chan_err.hide(true)
|
||||
elseif svr_c == nil then
|
||||
chan_err.set_value("Please set the supervisor channel.")
|
||||
@@ -211,54 +283,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=1,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_3,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"}
|
||||
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=8,text="Trusted Range (Wireless Only)"}
|
||||
self.range = NumberField{parent=net_c_3,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=net_c_3,y=10,height=4,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local n3_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_ct_tr()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
local range_val = tonumber(range.get_value())
|
||||
if timeout_val ~= nil and range_val ~= nil then
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
net_pane.set_value(3)
|
||||
p2_err.hide(true)
|
||||
elseif timeout_val == nil then
|
||||
p2_err.set_value("Please set the connection timeout.")
|
||||
p2_err.show()
|
||||
local range_val = tonumber(self.range.get_value())
|
||||
|
||||
if timeout_val == nil then
|
||||
n3_err.set_value("Please set the connection timeout.")
|
||||
n3_err.show()
|
||||
elseif tmp_cfg.WirelessModem and (range_val == nil) then
|
||||
n3_err.set_value("Please set the trusted range.")
|
||||
n3_err.show()
|
||||
else
|
||||
p2_err.set_value("Please set the trusted range.")
|
||||
p2_err.show()
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
tmp_cfg.TrustedRange = tri(tmp_cfg.WirelessModem, range_val, 0)
|
||||
|
||||
if tmp_cfg.WirelessModem then
|
||||
net_pane.set_value(4)
|
||||
else
|
||||
main_pane.set_value(4)
|
||||
tmp_cfg.AuthKey = ""
|
||||
end
|
||||
|
||||
n3_err.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
|
||||
local key, _ = TextField{parent=net_c_4,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) key.censor(util.trinary(enable, "*", nil)) end
|
||||
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
|
||||
|
||||
local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
@@ -269,8 +349,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -278,17 +358,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
TextBox{parent=log_cfg,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
|
||||
TextBox{parent=log_c_1,y=1,text="Please configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
TextBox{parent=log_c_1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=log_c_1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
local en_dbg = Checkbox{parent=log_c_1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
@@ -309,7 +389,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,y=14,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -321,17 +401,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
local clr_pane = MultiPane{parent=clr_cfg,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
TextBox{parent=clr_cfg,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=clr_c_1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_1,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
TextBox{parent=clr_c_2,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
@@ -360,15 +440,15 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_2,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local function back_from_colors()
|
||||
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
|
||||
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4))
|
||||
tool_ctl.jumped_to_color = false
|
||||
recolor(1)
|
||||
end
|
||||
@@ -402,7 +482,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
@@ -414,12 +494,12 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
clr_pane.set_value(1)
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_3,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -431,11 +511,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
local sum_pane = MultiPane{parent=summary,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
TextBox{parent=summary,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local setting_list = ListBox{parent=sum_c_1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_settings()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
@@ -471,10 +551,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
try_set(bundled, ini_cfg.EmerCoolColor ~= nil)
|
||||
if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end
|
||||
try_set(invert, ini_cfg.EmerCoolInvert)
|
||||
try_set(self.wireless, ini_cfg.WirelessModem)
|
||||
try_set(self.wired, ini_cfg.WiredModem ~= false)
|
||||
try_set(self.wl_pref, ini_cfg.PreferWireless)
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(plc_chan, ini_cfg.PLC_Channel)
|
||||
try_set(timeout, ini_cfg.ConnTimeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(self.range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
@@ -496,12 +579,12 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_2,x=1,y=3,text="Tip: you can run a Self-Check from the configurator home screen to make sure everything is going to work right!"}
|
||||
TextBox{parent=sum_c_2,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_2,y=3,text="Tip: you can run a Self-Check from the configurator home screen to make sure everything is going to work right!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
@@ -511,21 +594,21 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
TextBox{parent=sum_c_3,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/reactor-plc/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=sum_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
@@ -591,7 +674,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" and raw then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
val = util.strval(themes.fp_theme_name(raw))
|
||||
@@ -601,7 +684,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if (string.len(val) > val_max_w) or string.find(val, "\n") then
|
||||
@@ -614,7 +697,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
textbox = TextBox{parent=line,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
@@ -623,6 +706,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
end
|
||||
|
||||
-- generate the list of available/assigned wired modems
|
||||
function tool_ctl.gen_modem_list()
|
||||
modem_list.remove_all()
|
||||
|
||||
local enable = self.wired.get_value()
|
||||
|
||||
local function select(iface)
|
||||
tmp_cfg.WiredModem = iface
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
local modems = ppm.get_wired_modem_list()
|
||||
local missing = { tmp = true, ini = true }
|
||||
|
||||
for iface, _ in pairs(modems) do
|
||||
if ini_cfg.WiredModem == iface then missing.ini = false end
|
||||
if tmp_cfg.WiredModem == iface then missing.tmp = false end
|
||||
end
|
||||
|
||||
if missing.tmp and tmp_cfg.WiredModem then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
|
||||
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
|
||||
end
|
||||
|
||||
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
|
||||
-- list wired modems
|
||||
for iface, _ in pairs(modems) do
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == iface
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text=iface}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
@@ -33,7 +34,8 @@ local changes = {
|
||||
{ "v1.6.8", { "ConnTimeout can now have a fractional part" } },
|
||||
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } },
|
||||
{ "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
|
||||
{ "v1.8.21", { "Added option to invert emergency coolant redstone control" } }
|
||||
{ "v1.8.21", { "Added option to invert emergency coolant redstone control" } },
|
||||
{ "v1.10.0", { "Added support for wired communications modems" } }
|
||||
}
|
||||
|
||||
---@class plc_configurator
|
||||
@@ -68,6 +70,8 @@ local tool_ctl = {
|
||||
|
||||
gen_summary = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
gen_modem_list = function () end
|
||||
}
|
||||
|
||||
---@class plc_config
|
||||
@@ -78,6 +82,9 @@ local tmp_cfg = {
|
||||
EmerCoolSide = nil, ---@type string|nil
|
||||
EmerCoolColor = nil, ---@type color|nil
|
||||
EmerCoolInvert = false, ---@type boolean
|
||||
WirelessModem = true,
|
||||
WiredModem = false, ---@type string|false
|
||||
PreferWireless = true,
|
||||
SVR_Channel = nil, ---@type integer
|
||||
PLC_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
@@ -103,6 +110,9 @@ local fields = {
|
||||
{ "EmerCoolSide", "Emergency Coolant Side", nil },
|
||||
{ "EmerCoolColor", "Emergency Coolant Color", nil },
|
||||
{ "EmerCoolInvert", "Emergency Coolant Invert", false },
|
||||
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
|
||||
{ "WiredModem", "Wired Comms Modem", false },
|
||||
{ "PreferWireless", "Prefer Wireless Modem", true },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "PLC_Channel", "PLC Channel", 16241 },
|
||||
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||
@@ -142,18 +152,18 @@ local function config_view(display)
|
||||
|
||||
TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
local root_pane_div = Div{parent=display,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local plc_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
local check_sys = Div{parent=root_pane_div,x=1,y=1}
|
||||
local main_page = Div{parent=root_pane_div,y=1}
|
||||
local plc_cfg = Div{parent=root_pane_div,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,y=1}
|
||||
local summary = Div{parent=root_pane_div,y=1}
|
||||
local changelog = Div{parent=root_pane_div,y=1}
|
||||
local check_sys = Div{parent=root_pane_div,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,check_sys}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,check_sys}}
|
||||
|
||||
--#region Main Page
|
||||
|
||||
@@ -221,20 +231,20 @@ local function config_view(display)
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=changelog,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local c_log = ListBox{parent=cl,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=cl,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -261,8 +271,13 @@ function configurator.configure(ask_config)
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
-- set tmp_cfg so interface lists are correct
|
||||
tmp_cfg.WiredModem = ini_cfg.WiredModem
|
||||
|
||||
reset_term()
|
||||
|
||||
ppm.mount_all()
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
@@ -272,6 +287,8 @@ function configurator.configure(ask_config)
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
tool_ctl.gen_modem_list()
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
@@ -288,6 +305,16 @@ function configurator.configure(ask_config)
|
||||
display.handle_paste(param1)
|
||||
elseif event == "modem_message" then
|
||||
check.receive_sv(param1, param2, param3, param4, param5)
|
||||
elseif event == "conn_test_complete" then
|
||||
check.conn_test_callback(param1)
|
||||
elseif event == "peripheral_detach" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.handle_unmount(param1)
|
||||
tool_ctl.gen_modem_list()
|
||||
elseif event == "peripheral" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.mount(param1)
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
|
||||
@@ -11,11 +11,24 @@ local databus = {}
|
||||
-- databus PSIL
|
||||
databus.ps = psil.create()
|
||||
|
||||
local dbus_iface = {
|
||||
local _dbus = {
|
||||
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
|
||||
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
|
||||
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end,
|
||||
|
||||
degraded = false,
|
||||
wd_modem = true,
|
||||
wl_modem = true,
|
||||
coroutines = {}
|
||||
}
|
||||
|
||||
-- evaluate and publish system health status
|
||||
local function eval_status()
|
||||
local ok = (not _dbus.degraded) and _dbus.wd_modem and _dbus.wl_modem
|
||||
for _, v in pairs(_dbus.coroutines) do ok = ok and v end
|
||||
|
||||
databus.ps.publish("status", ok)
|
||||
end
|
||||
|
||||
-- call to toggle heartbeat signal
|
||||
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||
|
||||
@@ -23,17 +36,17 @@ function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||
---@param scram function reactor SCRAM function
|
||||
---@param reset function RPS reset function
|
||||
function databus.link_rps(scram, reset)
|
||||
dbus_iface.rps_scram = scram
|
||||
dbus_iface.rps_reset = reset
|
||||
_dbus.rps_scram = scram
|
||||
_dbus.rps_reset = reset
|
||||
end
|
||||
|
||||
-- transmit a command to the RPS to SCRAM
|
||||
function databus.rps_scram() dbus_iface.rps_scram() end
|
||||
function databus.rps_scram() _dbus.rps_scram() end
|
||||
|
||||
-- transmit a command to the RPS to reset
|
||||
function databus.rps_reset() dbus_iface.rps_reset() end
|
||||
function databus.rps_reset() _dbus.rps_reset() end
|
||||
|
||||
-- transmit firmware versions across the bus
|
||||
-- transmit firmware versions
|
||||
---@param plc_v string PLC version
|
||||
---@param comms_v string comms version
|
||||
function databus.tx_versions(plc_v, comms_v)
|
||||
@@ -41,41 +54,68 @@ function databus.tx_versions(plc_v, comms_v)
|
||||
databus.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- transmit unit ID across the bus
|
||||
-- transmit unit ID
|
||||
---@param id integer unit ID
|
||||
function databus.tx_id(id)
|
||||
databus.ps.publish("unit_id", id)
|
||||
end
|
||||
|
||||
-- transmit hardware status across the bus
|
||||
-- transmit hardware status
|
||||
---@param plc_state plc_state
|
||||
function databus.tx_hw_status(plc_state)
|
||||
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
|
||||
databus.ps.publish("has_modem", not plc_state.no_modem)
|
||||
databus.ps.publish("degraded", plc_state.degraded)
|
||||
databus.ps.publish("init_ok", plc_state.init_ok)
|
||||
databus.ps.publish("has_wd_modem", plc_state.wd_modem)
|
||||
databus.ps.publish("has_wl_modem", plc_state.wl_modem)
|
||||
|
||||
_dbus.degraded = plc_state.degraded
|
||||
_dbus.wd_modem = plc_state.wd_modem
|
||||
_dbus.wl_modem = plc_state.wl_modem
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit if the wired network is up
|
||||
---@param up boolean
|
||||
function databus.tx_wd_net(up)
|
||||
databus.ps.publish("has_wd_net", up)
|
||||
end
|
||||
|
||||
-- transmit if the wireless network is up
|
||||
---@param up boolean
|
||||
function databus.tx_wl_net(up)
|
||||
databus.ps.publish("has_wl_net", up)
|
||||
end
|
||||
|
||||
-- transmit if the reactor dangerously has multiple fission reactor logic adapters
|
||||
---@param multi boolean has multiple reactors
|
||||
function databus.tx_multi_reactor(multi)
|
||||
databus.ps.publish("has_multi_reactor", multi)
|
||||
end
|
||||
|
||||
-- transmit thread (routine) statuses
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function databus.tx_rt_status(thread, ok)
|
||||
databus.ps.publish(util.c("routine__", thread), ok)
|
||||
local name = util.c("routine__", thread)
|
||||
|
||||
databus.ps.publish(name, ok)
|
||||
|
||||
_dbus.coroutines[name] = ok
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit supervisor link state across the bus
|
||||
-- transmit supervisor link state
|
||||
---@param state integer
|
||||
function databus.tx_link_state(state)
|
||||
databus.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
-- transmit reactor enable state across the bus
|
||||
-- transmit reactor enable state
|
||||
---@param active any reactor active
|
||||
function databus.tx_reactor_state(active)
|
||||
databus.ps.publish("reactor_active", active == true)
|
||||
end
|
||||
|
||||
-- transmit RPS data across the bus
|
||||
-- transmit RPS data
|
||||
---@param tripped boolean RPS tripped
|
||||
---@param status boolean[] RPS status
|
||||
---@param emer_cool_active boolean RPS activated the emergency coolant
|
||||
@@ -95,11 +135,4 @@ function databus.tx_rps(tripped, status, emer_cool_active)
|
||||
databus.ps.publish("emer_cool", emer_cool_active)
|
||||
end
|
||||
|
||||
-- link a function to receive data from the bus
|
||||
---@param field string field name
|
||||
---@param func function function to link
|
||||
function databus.rx_field(field, func)
|
||||
databus.ps.subscribe(field, func)
|
||||
end
|
||||
|
||||
return databus
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-- Reactor PLC Front Panel GUI
|
||||
--
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
@@ -35,12 +36,11 @@ local ind_red = style.ind_red
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel DisplayBox main displaybox
|
||||
local function init(panel)
|
||||
---@param config plc_config configuraiton
|
||||
local function init(panel, config)
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
|
||||
local disabled_fg = style.fp.disabled_fg
|
||||
|
||||
local term_w, term_h = term.getSize()
|
||||
local term_w, _ = term.getSize()
|
||||
|
||||
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
|
||||
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
|
||||
@@ -51,15 +51,60 @@ local function init(panel)
|
||||
|
||||
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
|
||||
|
||||
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local sys_status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
|
||||
system.line_break()
|
||||
|
||||
init_ok.register(databus.ps, "init_ok", init_ok.update)
|
||||
sys_status.register(databus.ps, "status", sys_status.update)
|
||||
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
|
||||
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
|
||||
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
|
||||
|
||||
if config.Networked then
|
||||
if config.WirelessModem and config.WiredModem then
|
||||
local wd_modem = LEDPair{parent=system,label="WD MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
local wl_modem = LEDPair{parent=system,label="WL MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
local function wd_modem_update()
|
||||
if databus.ps.get("has_wd_modem") then
|
||||
if databus.ps.get("has_wd_net") then
|
||||
wd_modem.update(3)
|
||||
else wd_modem.update(2) end
|
||||
else wd_modem.update(1) end
|
||||
end
|
||||
|
||||
local function wl_modem_update()
|
||||
if databus.ps.get("has_wl_modem") then
|
||||
if databus.ps.get("has_wl_net") then
|
||||
wl_modem.update(3)
|
||||
else wl_modem.update(2) end
|
||||
else wl_modem.update(1) end
|
||||
end
|
||||
|
||||
wd_modem.register(databus.ps, "has_wd_modem", wd_modem_update)
|
||||
wd_modem.register(databus.ps, "has_wd_net", wd_modem_update)
|
||||
wl_modem.register(databus.ps, "has_wl_modem", wl_modem_update)
|
||||
wl_modem.register(databus.ps, "has_wl_net", wl_modem_update)
|
||||
else
|
||||
local modem = LEDPair{parent=system,label="MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
local pfx = util.trinary(config.WirelessModem, "has_wl_", "has_wd_")
|
||||
|
||||
local function modem_update()
|
||||
if databus.ps.get(pfx .. "modem") then
|
||||
if databus.ps.get(pfx .. "net") then
|
||||
modem.update(3)
|
||||
else modem.update(2) end
|
||||
else modem.update(1) end
|
||||
end
|
||||
|
||||
modem.register(databus.ps, pfx .. "modem", modem_update)
|
||||
modem.register(databus.ps, pfx .. "net", modem_update)
|
||||
end
|
||||
else
|
||||
local _ = LED{parent=system,label="MODEM",colors=ind_grn}
|
||||
end
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
|
||||
@@ -99,9 +144,6 @@ local function init(panel)
|
||||
|
||||
system.line_break()
|
||||
|
||||
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
|
||||
modem.register(databus.ps, "has_modem", modem.update)
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
|
||||
local rt_rps = LED{parent=system,label="RT RPS",colors=ind_grn}
|
||||
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=ind_grn}
|
||||
@@ -115,12 +157,8 @@ local function init(panel)
|
||||
rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
|
||||
rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=5,width=6,text=comp_id,fg_bg=disabled_fg}
|
||||
|
||||
--
|
||||
-- status & controls
|
||||
-- status & controls & hardware labeling
|
||||
--
|
||||
|
||||
local status = Div{parent=panel,width=term_w-32,height=18,x=17,y=3}
|
||||
@@ -133,11 +171,11 @@ local function init(panel)
|
||||
emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
|
||||
end
|
||||
|
||||
local status_trip_rct = Rectangle{parent=status,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
local status_trip_rct = Rectangle{parent=status,height=3,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
local status_trip = Div{parent=status_trip_rct,height=1,fg_bg=s_hi_box}
|
||||
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS}
|
||||
|
||||
local controls_rct = Rectangle{parent=status,width=status.get_width()-2,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
local controls_rct = Rectangle{parent=status,width=status.get_width()-2,height=3,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
local controls = Div{parent=controls_rct,width=controls_rct.get_width()-2,height=1,fg_bg=s_hi_box}
|
||||
local button_padding = math.floor((controls.get_width() - 14) / 3)
|
||||
PushButton{parent=controls,x=button_padding+1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
|
||||
@@ -146,16 +184,39 @@ local function init(panel)
|
||||
active.register(databus.ps, "reactor_active", active.update)
|
||||
scram.register(databus.ps, "rps_scram", scram.update)
|
||||
|
||||
--
|
||||
-- about footer
|
||||
--
|
||||
local hw_labels = Rectangle{parent=status,width=status.get_width()-2,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
|
||||
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
|
||||
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("%03d", os.getComputerID())
|
||||
|
||||
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
TextBox{parent=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
|
||||
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
|
||||
TextBox{parent=hw_labels,text="SN "..comp_id.."-PLC",fg_bg=s_hi_box}
|
||||
|
||||
-- warning about multiple reactors connected
|
||||
|
||||
local warn_strings = { "!! DANGER !!\n>1 REACTOR\nLOGIC ADAPTER", "REMOVE\nALL BUT ONE\nLOGIC ADAPTER" }
|
||||
local multi_warn = TextBox{parent=status,text=warn_strings[1],width=status.get_width()-2,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.red),hidden=true}
|
||||
|
||||
local warn_toggle = true
|
||||
local function flash_warn()
|
||||
multi_warn.recolor(util.trinary(warn_toggle, colors.black, colors.yellow))
|
||||
multi_warn.set_value(util.trinary(warn_toggle, warn_strings[2], warn_strings[1]))
|
||||
warn_toggle = not warn_toggle
|
||||
|
||||
if databus.ps.get("has_multi_reactor") then tcd.dispatch_unique(2, flash_warn) end
|
||||
end
|
||||
|
||||
multi_warn.register(databus.ps, "has_multi_reactor", function (v)
|
||||
if v then
|
||||
multi_warn.show()
|
||||
warn_toggle = false
|
||||
flash_warn()
|
||||
else
|
||||
tcd.abort(flash_warn)
|
||||
multi_warn.hide(true)
|
||||
end
|
||||
end)
|
||||
|
||||
--
|
||||
-- rps list
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
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 comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
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 themes = require("graphics.themes")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
local backplane = require("reactor-plc.backplane")
|
||||
local databus = require("reactor-plc.databus")
|
||||
|
||||
local plc = {}
|
||||
|
||||
@@ -27,6 +28,10 @@ local RPS_LIMITS = const.RPS_LIMITS
|
||||
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
|
||||
local PCALL_START_MSG = "Reactor is already active."
|
||||
|
||||
-- wait 5 seconds after initializing a network switch request before being allowed to send more,
|
||||
-- which avoids repeat duplicate requests
|
||||
local FAILOVER_GRACE_PERIOD_MS = 5000
|
||||
|
||||
---@type plc_config
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local config = {}
|
||||
@@ -45,6 +50,9 @@ function plc.load_config()
|
||||
config.EmerCoolColor = settings.get("EmerCoolColor")
|
||||
config.EmerCoolInvert = settings.get("EmerCoolInvert")
|
||||
|
||||
config.WirelessModem = settings.get("WirelessModem")
|
||||
config.WiredModem = settings.get("WiredModem")
|
||||
config.PreferWireless = settings.get("PreferWireless")
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.PLC_Channel = settings.get("PLC_Channel")
|
||||
config.ConnTimeout = settings.get("ConnTimeout")
|
||||
@@ -70,7 +78,11 @@ function plc.validate_config(cfg)
|
||||
cfv.assert_type_int(cfg.UnitID)
|
||||
cfv.assert_type_bool(cfg.EmerCoolEnable)
|
||||
|
||||
if cfg.Networked == true then
|
||||
if cfg.Networked then
|
||||
cfv.assert_type_bool(cfg.WirelessModem)
|
||||
cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string"))
|
||||
cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string"))
|
||||
cfv.assert_type_bool(cfg.PreferWireless)
|
||||
cfv.assert_channel(cfg.SVR_Channel)
|
||||
cfv.assert_channel(cfg.PLC_Channel)
|
||||
cfv.assert_type_num(cfg.ConnTimeout)
|
||||
@@ -115,10 +127,9 @@ function plc.rps_init(reactor, is_formed)
|
||||
local self = {
|
||||
---@type boolean[] check states
|
||||
state = { false, false, false, false, false, false, false, false, false, false, false, false },
|
||||
reactor_enabled = false,
|
||||
enabled_at = 0,
|
||||
reactor_active = false,
|
||||
emer_cool_active = nil, ---@type boolean
|
||||
formed = is_formed,
|
||||
formed = is_formed, ---@type boolean|nil
|
||||
force_disabled = false,
|
||||
tripped = false,
|
||||
trip_cause = "ok" ---@type rps_trip_cause
|
||||
@@ -284,6 +295,14 @@ function plc.rps_init(reactor, is_formed)
|
||||
reactor = new_reactor
|
||||
end
|
||||
|
||||
-- check the reactor enable state and update the internal tracking flag
|
||||
---@return boolean active
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function public.check_active()
|
||||
self.reactor_active = reactor.getStatus() == true
|
||||
return self.reactor_active
|
||||
end
|
||||
|
||||
-- trip for lost peripheral
|
||||
function public.trip_fault()
|
||||
_set_fault()
|
||||
@@ -320,11 +339,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_SCRAM_MSG) then
|
||||
log.error("RPS: failed reactor SCRAM")
|
||||
return false
|
||||
else
|
||||
self.reactor_enabled = false
|
||||
self.last_runtime = util.time_ms() - self.enabled_at
|
||||
return true
|
||||
end
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- start the reactor now<br>
|
||||
@@ -337,11 +352,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
reactor.activate()
|
||||
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_START_MSG) then
|
||||
log.error("RPS: failed reactor start")
|
||||
else
|
||||
self.reactor_enabled = true
|
||||
self.enabled_at = util.time_ms()
|
||||
return true
|
||||
end
|
||||
else return true end
|
||||
else
|
||||
log.debug(util.c("RPS: failed start, RPS tripped: ", self.trip_cause))
|
||||
end
|
||||
@@ -351,6 +362,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
|
||||
-- automatic control activate/re-activate
|
||||
---@return boolean success
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function public.auto_activate()
|
||||
-- clear automatic SCRAM if it was the cause
|
||||
if self.tripped and self.trip_cause == "automatic" then
|
||||
@@ -364,29 +376,36 @@ function plc.rps_init(reactor, is_formed)
|
||||
return public.activate()
|
||||
end
|
||||
|
||||
-- check all safety conditions
|
||||
-- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions
|
||||
---@nodiscard
|
||||
---@param has_reactor boolean if the PLC state indicates we have a reactor
|
||||
---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip
|
||||
function public.check()
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function public.check(has_reactor)
|
||||
local status = RPS_TRIP_CAUSE.OK
|
||||
local was_tripped = self.tripped
|
||||
local first_trip = false
|
||||
|
||||
if self.formed then
|
||||
-- update state
|
||||
parallel.waitForAll(
|
||||
_is_formed,
|
||||
_is_force_disabled,
|
||||
_high_damage,
|
||||
_high_temp,
|
||||
_low_coolant,
|
||||
_excess_waste,
|
||||
_excess_heated_coolant,
|
||||
_insufficient_fuel
|
||||
)
|
||||
if has_reactor then
|
||||
if self.formed then
|
||||
-- update state
|
||||
parallel.waitForAll(
|
||||
_is_formed,
|
||||
_is_force_disabled,
|
||||
_high_damage,
|
||||
_high_temp,
|
||||
_low_coolant,
|
||||
_excess_waste,
|
||||
_excess_heated_coolant,
|
||||
_insufficient_fuel
|
||||
)
|
||||
else
|
||||
-- check to see if its now formed
|
||||
_is_formed()
|
||||
end
|
||||
else
|
||||
-- check to see if its now formed
|
||||
_is_formed()
|
||||
self.formed = nil
|
||||
self.state[CHK.SYS_FAIL] = true
|
||||
end
|
||||
|
||||
-- check system states in order of severity
|
||||
@@ -433,20 +452,16 @@ function plc.rps_init(reactor, is_formed)
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
end
|
||||
|
||||
-- if a new trip occured...
|
||||
-- SCRAM on a new trip, RPS thread handles reactor becoming active while already tripped
|
||||
if (not was_tripped) and (status ~= RPS_TRIP_CAUSE.OK) then
|
||||
first_trip = true
|
||||
self.tripped = true
|
||||
self.trip_cause = status
|
||||
|
||||
-- in the case that the reactor is detected to be active,
|
||||
-- it will be scrammed shortly after this in the main RPS loop if we don't here
|
||||
if self.formed then
|
||||
if not self.force_disabled then
|
||||
public.scram()
|
||||
else
|
||||
if self.force_disabled then
|
||||
log.warning("RPS: skipping SCRAM due to reactor being force disabled")
|
||||
end
|
||||
else public.scram() end
|
||||
else
|
||||
log.warning("RPS: skipping SCRAM due to not being formed")
|
||||
end
|
||||
@@ -472,17 +487,13 @@ function plc.rps_init(reactor, is_formed)
|
||||
function public.is_low_coolant() return self.states[CHK.LOW_COOLANT] end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_active() return self.reactor_enabled end
|
||||
function public.is_active() return self.reactor_active end
|
||||
---@nodiscard
|
||||
---@return boolean|nil formed true if formed, false if not, nil if unknown
|
||||
function public.is_formed() return self.formed end
|
||||
---@nodiscard
|
||||
function public.is_force_disabled() return self.force_disabled end
|
||||
|
||||
-- get the runtime of the reactor if active, or the last runtime if disabled
|
||||
---@nodiscard
|
||||
---@return integer runtime time since last enable
|
||||
function public.get_runtime() return util.trinary(self.reactor_enabled, util.time_ms() - self.enabled_at, self.last_runtime) end
|
||||
|
||||
-- reset the RPS
|
||||
---@param quiet? boolean true to suppress the info log message
|
||||
function public.reset(quiet)
|
||||
@@ -495,14 +506,14 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
|
||||
-- partial RPS reset that only clears fault and sys_fail
|
||||
function public.reset_formed()
|
||||
function public.reset_reattach()
|
||||
self.tripped = false
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
|
||||
self.state[CHK.FAULT] = false
|
||||
self.state[CHK.SYS_FAIL] = false
|
||||
|
||||
log.info("RPS: partial reset on formed")
|
||||
log.info("RPS: partial reset on connected or formed")
|
||||
end
|
||||
|
||||
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
|
||||
@@ -527,17 +538,18 @@ end
|
||||
-- Reactor PLC Communications
|
||||
---@nodiscard
|
||||
---@param version string PLC version
|
||||
---@param nic nic network interface device
|
||||
---@param tx_nic nic network interface device
|
||||
---@param reactor table reactor device
|
||||
---@param rps rps RPS reference
|
||||
---@param conn_watchdog watchdog watchdog reference
|
||||
function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
function plc.comms(version, tx_nic, reactor, rps, conn_watchdog)
|
||||
local self = {
|
||||
sv_addr = comms.BROADCAST,
|
||||
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
scrammed = false,
|
||||
linked = false,
|
||||
failover_init = 0,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
resend_build = false,
|
||||
auto_ack_token = 0,
|
||||
@@ -545,25 +557,22 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
max_burn_rate = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
if config.WirelessModem then
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
end
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(config.PLC_Channel)
|
||||
--#region PRIVATE FUNCTIONS --
|
||||
|
||||
-- send an RPLC packet
|
||||
---@param msg_type RPLC_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local r_pkt = comms.rplc_packet()
|
||||
local frame, rplc = comms.scada_frame(), comms.rplc_container()
|
||||
|
||||
r_pkt.make(config.UnitID, msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
|
||||
rplc.make(config.UnitID, msg_type, msg)
|
||||
frame.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, rplc.raw_packet())
|
||||
|
||||
nic.transmit(config.SVR_Channel, config.PLC_Channel, s_pkt)
|
||||
tx_nic.transmit(config.SVR_Channel, config.PLC_Channel, frame)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -571,24 +580,19 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
nic.transmit(config.SVR_Channel, config.PLC_Channel, s_pkt)
|
||||
tx_nic.transmit(config.SVR_Channel, config.PLC_Channel, frame)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- dynamic reactor status information, excluding heating rate
|
||||
---@return table data_table, boolean faulted
|
||||
local function _get_reactor_status()
|
||||
local fuel = nil
|
||||
local waste = nil
|
||||
local coolant = nil
|
||||
local hcoolant = nil
|
||||
|
||||
local fuel, waste, coolant, hcoolant = nil, nil, nil, nil
|
||||
local data_table = {}
|
||||
|
||||
reactor.__p_disable_afc()
|
||||
@@ -707,11 +711,151 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
reactor.__p_enable_afc()
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
-- handle a burn rate command
|
||||
---@param packet rplc_packet
|
||||
---@param setpoints plc_setpoints
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
local function _handle_burn_rate(packet, setpoints)
|
||||
if (packet.length == 2) and (type(packet.data[1]) == "number") then
|
||||
local success = false
|
||||
local burn_rate = math.floor(packet.data[1] * 10) / 10
|
||||
local ramp = packet.data[2]
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = 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
|
||||
reactor.setBurnRate(burn_rate)
|
||||
success = reactor.__p_is_ok()
|
||||
end
|
||||
else
|
||||
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
|
||||
end
|
||||
end
|
||||
|
||||
_send_ack(packet.type, success)
|
||||
else
|
||||
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle an auto burn rate command
|
||||
---@param packet rplc_packet
|
||||
---@param setpoints plc_setpoints
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
local function _handle_auto_burn_rate(packet, setpoints)
|
||||
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
|
||||
local ack = AUTO_ACK.FAIL
|
||||
local burn_rate = math.floor(packet.data[1] * 100) / 100
|
||||
local ramp = packet.data[2]
|
||||
self.auto_ack_token = packet.data[3]
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = 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.01 then
|
||||
if rps.is_active() then
|
||||
-- auto scram to disable
|
||||
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
|
||||
if rps.scram() then
|
||||
ack = AUTO_ACK.ZERO_DIS_OK
|
||||
else
|
||||
log.warning("AUTO: automatic reactor stop failed")
|
||||
end
|
||||
else
|
||||
ack = AUTO_ACK.ZERO_DIS_OK
|
||||
end
|
||||
elseif burn_rate <= self.max_burn_rate then
|
||||
if not rps.is_active() then
|
||||
-- activate the reactor
|
||||
log.debug("AUTO: activating the reactor")
|
||||
|
||||
reactor.setBurnRate(0.01)
|
||||
if reactor.__p_is_faulted() then
|
||||
log.warning("AUTO: failed to reset burn rate for auto activation")
|
||||
else
|
||||
if not rps.auto_activate() then
|
||||
log.warning("AUTO: automatic reactor activation failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- if active, set/ramp burn rate
|
||||
if rps.is_active() then
|
||||
if ramp then
|
||||
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
|
||||
setpoints.burn_rate_en = true
|
||||
setpoints.burn_rate = burn_rate
|
||||
ack = AUTO_ACK.RAMP_SET_OK
|
||||
else
|
||||
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
|
||||
reactor.setBurnRate(burn_rate)
|
||||
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
|
||||
end
|
||||
end
|
||||
|
||||
_send_ack(packet.type, ack)
|
||||
else
|
||||
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region PUBLIC FUNCTIONS --
|
||||
|
||||
---@class plc_comms
|
||||
local public = {}
|
||||
|
||||
-- switch the current active NIC
|
||||
---@param new_nic nic
|
||||
function public.switch_nic(new_nic)
|
||||
if tx_nic.is_connected() then
|
||||
-- try to gracefully switch, we have an intact continuous connection
|
||||
log.info(util.c("switching link to reconnected interface ", new_nic.phy_name(), " from ", tx_nic.phy_name()))
|
||||
|
||||
tx_nic = new_nic
|
||||
_send_mgmt(MGMT_TYPE.SWITCH_NET, {})
|
||||
else
|
||||
-- can't gracefully switch, the other NIC was lost
|
||||
log.info(util.c("closing link on ", tx_nic.phy_name(), ", switching to ", new_nic.phy_name()))
|
||||
|
||||
tx_nic = new_nic
|
||||
conn_watchdog.cancel()
|
||||
public.unlink()
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the provided NIC is currently active, and if not, switch back to it
|
||||
---@param act_nic nic
|
||||
function public.manage_failover(act_nic)
|
||||
if (act_nic ~= tx_nic) and act_nic.is_network_up() and ((util.time_ms() - self.failover_init) > FAILOVER_GRACE_PERIOD_MS) then
|
||||
log.info(util.c("primary interface ", act_nic.phy_name(), " is up, requesting link switch"))
|
||||
|
||||
tx_nic = act_nic
|
||||
_send_mgmt(MGMT_TYPE.SWITCH_NET, {})
|
||||
|
||||
self.failover_init = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
-- reconnect a newly connected reactor
|
||||
---@param new_reactor table
|
||||
function public.reconnect_reactor(new_reactor)
|
||||
@@ -733,14 +877,20 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
conn_watchdog.cancel()
|
||||
public.unlink()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
public.unlink()
|
||||
end
|
||||
|
||||
-- attempt to establish link with supervisor
|
||||
function public.send_link_req()
|
||||
---@param nic nic nic to transmit on
|
||||
function public.send_link_req(nic)
|
||||
local ini_nic = tx_nic
|
||||
tx_nic = nic
|
||||
|
||||
self.r_seq_num = nil
|
||||
_send_mgmt(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, config.UnitID })
|
||||
|
||||
tx_nic = ini_nic
|
||||
end
|
||||
|
||||
-- send live status information
|
||||
@@ -748,8 +898,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
---@param formed boolean reactor formed (from PLC state)
|
||||
function public.send_status(no_reactor, formed)
|
||||
if self.linked then
|
||||
local mek_data = nil ---@type table
|
||||
local heating_rate = 0.0 ---@type number
|
||||
local mek_data = nil ---@type table
|
||||
local heating_rate = 0.0 ---@type number
|
||||
|
||||
if (not no_reactor) and rps.is_formed() then
|
||||
if _update_status_cache() then mek_data = self.status_cache end
|
||||
@@ -794,45 +944,39 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return rplc_frame|mgmt_frame|nil packet
|
||||
---@return rplc_packet|mgmt_packet|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local pkt, nic = nil, backplane.nics[side]
|
||||
|
||||
if s_pkt then
|
||||
-- get as RPLC packet
|
||||
if s_pkt.protocol() == PROTOCOL.RPLC then
|
||||
local rplc_pkt = comms.rplc_packet()
|
||||
if rplc_pkt.decode(s_pkt) then
|
||||
pkt = rplc_pkt.get()
|
||||
if nic then
|
||||
local frame = nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if frame then
|
||||
if frame.protocol() == PROTOCOL.RPLC then
|
||||
pkt = comms.rplc_container().decode(frame)
|
||||
elseif frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_container().decode(frame)
|
||||
else
|
||||
log.debug("unsupported packet type " .. frame.protocol(), true)
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("unsupported packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
else
|
||||
log.error("parse_packet(" .. side .. "): received a packet from an interface without a nic?")
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle RPLC and MGMT packets
|
||||
---@param packet rplc_frame|mgmt_frame packet frame
|
||||
---@param packet rplc_packet|mgmt_packet packet frame
|
||||
---@param plc_state plc_state PLC state
|
||||
---@param setpoints setpoints setpoint control table
|
||||
function public.handle_packet(packet, plc_state, setpoints)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
---@param setpoints plc_setpoints setpoint control table
|
||||
---@param println_ts function console print, when UI isn't running
|
||||
function public.handle_packet(packet, plc_state, setpoints, println_ts)
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
-- handle packets now that we have prints setup
|
||||
if l_chan == config.PLC_Channel then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
@@ -853,7 +997,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.RPLC then
|
||||
---@cast packet rplc_frame
|
||||
---@cast packet rplc_packet
|
||||
-- if linked, only accept packets from configured supervisor
|
||||
if self.linked then
|
||||
if packet.type == RPLC_TYPE.STATUS then
|
||||
@@ -867,36 +1011,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
log.debug("sent out structure again, did supervisor miss it?")
|
||||
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
|
||||
-- set the burn rate
|
||||
if (packet.length == 2) and (type(packet.data[1]) == "number") then
|
||||
local success = false
|
||||
local burn_rate = math.floor(packet.data[1] * 10) / 10
|
||||
local ramp = packet.data[2]
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = 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
|
||||
reactor.setBurnRate(burn_rate)
|
||||
success = reactor.__p_is_ok()
|
||||
end
|
||||
else
|
||||
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
|
||||
end
|
||||
end
|
||||
|
||||
_send_ack(packet.type, success)
|
||||
else
|
||||
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
|
||||
end
|
||||
_handle_burn_rate(packet, setpoints)
|
||||
elseif packet.type == RPLC_TYPE.RPS_ENABLE then
|
||||
-- enable the reactor
|
||||
self.scrammed = false
|
||||
@@ -925,68 +1040,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
|
||||
-- automatic control requested a new burn rate
|
||||
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
|
||||
local ack = AUTO_ACK.FAIL
|
||||
local burn_rate = math.floor(packet.data[1] * 100) / 100
|
||||
local ramp = packet.data[2]
|
||||
self.auto_ack_token = packet.data[3]
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = 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.01 then
|
||||
if rps.is_active() then
|
||||
-- auto scram to disable
|
||||
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
|
||||
if rps.scram() then
|
||||
ack = AUTO_ACK.ZERO_DIS_OK
|
||||
else
|
||||
log.warning("AUTO: automatic reactor stop failed")
|
||||
end
|
||||
else
|
||||
ack = AUTO_ACK.ZERO_DIS_OK
|
||||
end
|
||||
elseif burn_rate <= self.max_burn_rate then
|
||||
if not rps.is_active() then
|
||||
-- activate the reactor
|
||||
log.debug("AUTO: activating the reactor")
|
||||
|
||||
reactor.setBurnRate(0.01)
|
||||
if reactor.__p_is_faulted() then
|
||||
log.warning("AUTO: failed to reset burn rate for auto activation")
|
||||
else
|
||||
if not rps.auto_activate() then
|
||||
log.warning("AUTO: automatic reactor activation failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- if active, set/ramp burn rate
|
||||
if rps.is_active() then
|
||||
if ramp then
|
||||
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
|
||||
setpoints.burn_rate_en = true
|
||||
setpoints.burn_rate = burn_rate
|
||||
ack = AUTO_ACK.RAMP_SET_OK
|
||||
else
|
||||
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
|
||||
reactor.setBurnRate(burn_rate)
|
||||
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
|
||||
end
|
||||
end
|
||||
|
||||
_send_ack(packet.type, ack)
|
||||
else
|
||||
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
|
||||
end
|
||||
_handle_auto_burn_rate(packet, setpoints)
|
||||
else
|
||||
log.debug("received unknown RPLC packet type " .. packet.type)
|
||||
end
|
||||
@@ -994,7 +1048,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
log.debug("discarding RPLC packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
---@cast packet mgmt_packet
|
||||
-- if linked, only accept packets from configured supervisor
|
||||
if self.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
@@ -1028,8 +1082,10 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
tx_nic = backplane.nics[packet.scada_frame.interface()]
|
||||
|
||||
println_ts("linked!")
|
||||
log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")")
|
||||
log.info(util.c("supervisor establish request approved, linked to SV (CID#", src_addr, ") on ", tx_nic.phy_name()))
|
||||
|
||||
-- link + reset cache
|
||||
self.sv_addr = src_addr
|
||||
@@ -1086,6 +1142,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
---@nodiscard
|
||||
function public.is_linked() return self.linked end
|
||||
|
||||
--#endregion
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
@@ -18,15 +18,14 @@ local ui = {
|
||||
}
|
||||
|
||||
-- try to start the UI
|
||||
---@param theme FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE color mode
|
||||
---@param config plc_config configuration
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui(theme, color_mode)
|
||||
function renderer.try_start_ui(config)
|
||||
local status, msg = true, nil
|
||||
|
||||
if ui.display == nil then
|
||||
-- set theme
|
||||
style.set_theme(theme, color_mode)
|
||||
style.set_theme(config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
@@ -40,7 +39,7 @@ function renderer.try_start_ui(theme, color_mode)
|
||||
end
|
||||
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.theme.color_modes[color_mode]
|
||||
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
@@ -48,7 +47,7 @@ function renderer.try_start_ui(theme, color_mode)
|
||||
-- init front panel view
|
||||
status, msg = pcall(function ()
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
|
||||
panel_view(ui.display)
|
||||
panel_view(ui.display, config)
|
||||
end)
|
||||
|
||||
if status then
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
local backplane = require("reactor-plc.backplane")
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
@@ -18,7 +19,7 @@ local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "v1.8.22"
|
||||
local R_PLC_VERSION = "v1.11.1"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -66,7 +67,7 @@ local function main()
|
||||
-- startup
|
||||
----------------------------------------
|
||||
|
||||
-- record firmware versions and ID
|
||||
-- report versions and ID
|
||||
databus.tx_versions(R_PLC_VERSION, comms.version)
|
||||
databus.tx_id(config.UnitID)
|
||||
|
||||
@@ -87,32 +88,32 @@ local function main()
|
||||
-- PLC system state flags
|
||||
---@class plc_state
|
||||
plc_state = {
|
||||
init_ok = true,
|
||||
fp_ok = false,
|
||||
shutdown = false,
|
||||
degraded = true,
|
||||
reactor_formed = true,
|
||||
no_reactor = true,
|
||||
no_modem = true
|
||||
reactor_formed = true,
|
||||
wd_modem = true,
|
||||
wl_modem = true
|
||||
},
|
||||
|
||||
-- control setpoints
|
||||
---@class setpoints
|
||||
---@class plc_setpoints
|
||||
setpoints = {
|
||||
burn_rate_en = false,
|
||||
burn_rate = 0.0
|
||||
},
|
||||
|
||||
-- core PLC devices
|
||||
-- global PLC devices, still initialized by the backplane
|
||||
---@class plc_dev
|
||||
plc_dev = {
|
||||
reactor = ppm.get_fission_reactor(),
|
||||
modem = ppm.get_wireless_modem()
|
||||
reactor = nil ---@type table
|
||||
},
|
||||
|
||||
-- system objects
|
||||
---@class plc_sys
|
||||
plc_sys = {
|
||||
rps = nil, ---@type rps
|
||||
nic = nil, ---@type nic
|
||||
plc_comms = nil, ---@type plc_comms
|
||||
conn_watchdog = nil ---@type watchdog
|
||||
},
|
||||
@@ -122,6 +123,19 @@ local function main()
|
||||
mq_rps = mqueue.new(),
|
||||
mq_comms_tx = mqueue.new(),
|
||||
mq_comms_rx = mqueue.new()
|
||||
},
|
||||
|
||||
-- message queue message types
|
||||
q_types = {
|
||||
MQ__RPS_CMD = {
|
||||
SCRAM = 1,
|
||||
DEGRADED_SCRAM = 2,
|
||||
TRIP_TIMEOUT = 3,
|
||||
RESET_REATTACH = 4
|
||||
},
|
||||
MQ__COMM_CMD = {
|
||||
SEND_STATUS = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,110 +144,65 @@ local function main()
|
||||
|
||||
local plc_state = __shared_memory.plc_state
|
||||
|
||||
-- initial state evaluation
|
||||
plc_state.no_reactor = smem_dev.reactor == nil
|
||||
plc_state.no_modem = smem_dev.modem == nil
|
||||
-- reactor and modem initialization
|
||||
backplane.init(config, __shared_memory)
|
||||
|
||||
-- we need a reactor, can at least do some things even if it isn't formed though
|
||||
if plc_state.no_reactor then
|
||||
println("init> fission reactor not found")
|
||||
log.warning("init> no reactor on startup")
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
elseif not smem_dev.reactor.isFormed() then
|
||||
println("init> fission reactor is not formed")
|
||||
log.warning("init> reactor logic adapter present, but reactor is not formed")
|
||||
|
||||
plc_state.degraded = true
|
||||
plc_state.reactor_formed = false
|
||||
-- scram on boot if networked, otherwise leave the reactor be
|
||||
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
log.debug("startup> power-on SCRAM")
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
|
||||
-- modem is required if networked
|
||||
if __shared_memory.networked and plc_state.no_modem then
|
||||
println("init> wireless modem not found")
|
||||
log.warning("init> no wireless modem on startup")
|
||||
-- setup front panel
|
||||
local message
|
||||
plc_state.fp_ok, message = renderer.try_start_ui(config)
|
||||
|
||||
-- scram reactor if present and enabled
|
||||
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
-- ...or not
|
||||
if not plc_state.fp_ok then
|
||||
println_ts(util.c("UI error: ", message))
|
||||
println("startup> running without front panel")
|
||||
log.error(util.c("front panel GUI render failed with error ", message))
|
||||
log.info("startup> running in headless mode without front panel")
|
||||
end
|
||||
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
|
||||
local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) end end
|
||||
|
||||
-- PLC init<br>
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
local function init()
|
||||
-- scram on boot if networked, otherwise leave the reactor be
|
||||
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
----------------------------------------
|
||||
-- initialize PLC
|
||||
----------------------------------------
|
||||
|
||||
-- setup front panel
|
||||
if not renderer.ui_ready() then
|
||||
local message
|
||||
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
|
||||
-- init reactor protection system
|
||||
smem_sys.rps = plc.rps_init(smem_dev.reactor, util.trinary(plc_state.no_reactor, nil, plc_state.reactor_formed))
|
||||
log.debug("startup> rps init")
|
||||
|
||||
-- ...or not
|
||||
if not plc_state.fp_ok then
|
||||
println_ts(util.c("UI error: ", message))
|
||||
println("init> running without front panel")
|
||||
log.error(util.c("front panel GUI render failed with error ", message))
|
||||
log.info("init> running in headless mode without front panel")
|
||||
end
|
||||
end
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- init reactor protection system
|
||||
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed)
|
||||
log.debug("init> rps init")
|
||||
|
||||
if __shared_memory.networked then
|
||||
-- comms watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
|
||||
log.debug("init> conn watchdog started")
|
||||
|
||||
-- create network interface then setup comms
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
|
||||
log.debug("init> comms init")
|
||||
else
|
||||
_println_no_fp("init> starting in offline mode")
|
||||
log.info("init> running without networking")
|
||||
end
|
||||
|
||||
-- notify user of emergency coolant configuration status
|
||||
if config.EmerCoolEnable then
|
||||
println("init> emergency coolant control ready")
|
||||
log.info("init> running with emergency coolant control available")
|
||||
end
|
||||
|
||||
util.push_event("clock_start")
|
||||
|
||||
_println_no_fp("init> completed")
|
||||
log.info("init> startup completed")
|
||||
else
|
||||
_println_no_fp("init> system in degraded state, awaiting devices...")
|
||||
log.warning("init> started in a degraded state, awaiting peripheral connections...")
|
||||
end
|
||||
|
||||
databus.tx_hw_status(plc_state)
|
||||
-- notify user of emergency coolant configuration status
|
||||
if config.EmerCoolEnable then
|
||||
_println_no_fp("startup> emergency coolant control ready")
|
||||
log.info("startup> emergency coolant control available")
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- start system
|
||||
----------------------------------------
|
||||
-- conditionally init comms
|
||||
if __shared_memory.networked then
|
||||
-- comms watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
|
||||
log.debug("startup> conn watchdog started")
|
||||
|
||||
-- initialize PLC
|
||||
init()
|
||||
-- create network interface then setup comms
|
||||
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, backplane.active_nic(), smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
else
|
||||
_println_no_fp("startup> starting in non-networked mode")
|
||||
log.info("startup> starting without networking")
|
||||
end
|
||||
|
||||
databus.tx_hw_status(plc_state)
|
||||
|
||||
_println_no_fp("startup> completed")
|
||||
log.info("startup> completed")
|
||||
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory, init)
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local rps_thread = threads.thread__rps(__shared_memory)
|
||||
|
||||
if __shared_memory.networked then
|
||||
@@ -247,14 +216,12 @@ local function main()
|
||||
-- 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.no_reactor, plc_state.reactor_formed)
|
||||
smem_sys.plc_comms.send_rps_status()
|
||||
-- send status one last time after RPS shutdown
|
||||
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
smem_sys.plc_comms.send_rps_status()
|
||||
|
||||
-- close connection
|
||||
smem_sys.plc_comms.close()
|
||||
end
|
||||
-- close connection
|
||||
smem_sys.plc_comms.close()
|
||||
else
|
||||
-- run threads, excluding comms
|
||||
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local backplane = require("reactor-plc.backplane")
|
||||
local databus = require("reactor-plc.databus")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
|
||||
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
|
||||
local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks)
|
||||
local MAIN_CLOCK = 0.5 -- 2Hz, 10 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
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
---@param init function
|
||||
function threads.thread__main(smem, init)
|
||||
function threads.thread__main(smem)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
@@ -42,218 +32,140 @@ function threads.thread__main(smem, init)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("main", true)
|
||||
log.debug("main thread init, clock inactive")
|
||||
log.debug("OS: main thread start")
|
||||
|
||||
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
|
||||
-- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks)
|
||||
local LINK_TICKS = 8
|
||||
local LINK_TICKS = 2
|
||||
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
|
||||
local networked = smem.networked
|
||||
local plc_state = smem.plc_state
|
||||
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
local conn_watchdog = smem.plc_sys.conn_watchdog
|
||||
|
||||
local MQ__RPS_CMD = smem.q_types.MQ__RPS_CMD
|
||||
local MQ__COMM_CMD = smem.q_types.MQ__COMM_CMD
|
||||
|
||||
-- main loop periodic tasks
|
||||
local function loop_tick()
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- periodic hardware tasks
|
||||
backplane.periodic()
|
||||
|
||||
-- send updated data or try to link
|
||||
if networked then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
|
||||
plc_comms.manage_failover(backplane.active_nic())
|
||||
elseif ticks_to_update == 0 then
|
||||
local a_nic, s_nic = backplane.active_nic(), backplane.standby_nic()
|
||||
|
||||
if a_nic.is_network_up() then
|
||||
plc_comms.send_link_req(a_nic)
|
||||
elseif s_nic and s_nic.is_network_up() then
|
||||
plc_comms.send_link_req(s_nic)
|
||||
end
|
||||
|
||||
ticks_to_update = LINK_TICKS
|
||||
else
|
||||
ticks_to_update = ticks_to_update - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- check for formed state change
|
||||
if (not plc_state.reactor_formed) and rps.is_formed() then
|
||||
-- reactor now formed
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
println_ts("reactor is now formed")
|
||||
log.info("reactor is now formed")
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or backplane.active_nic().is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
-- without this, auto control can't resume on chunk load
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.RESET_REATTACH)
|
||||
elseif plc_state.reactor_formed and (rps.is_formed() == false) then
|
||||
-- reactor no longer formed
|
||||
println_ts("reactor is no longer formed")
|
||||
log.info("reactor is no longer formed")
|
||||
|
||||
plc_state.reactor_formed = false
|
||||
plc_state.degraded = true
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
end
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
-- 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 nic = smem.plc_sys.nic
|
||||
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
|
||||
-- note: loop clock is only running if init_ok = true
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- send updated data
|
||||
if networked and nic.is_connected() 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
|
||||
|
||||
-- check for formed state change
|
||||
if (not plc_state.reactor_formed) and rps.is_formed() then
|
||||
-- reactor now formed
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
println_ts("reactor is now formed.")
|
||||
log.info("reactor is now formed")
|
||||
|
||||
-- SCRAM newly formed reactor
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or nic.is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
elseif plc_state.reactor_formed and not rps.is_formed() then
|
||||
-- reactor no longer formed
|
||||
println_ts("reactor is no longer formed.")
|
||||
log.info("reactor is no longer formed")
|
||||
|
||||
plc_state.reactor_formed = false
|
||||
plc_state.degraded = true
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then
|
||||
if event == "modem_message" and networked 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)
|
||||
smem.q.mq_comms_rx.push_network(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? close connection and shutdown reactor
|
||||
plc_comms.close()
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
|
||||
elseif event == "timer" then
|
||||
-- notify timer callback dispatcher if no other timer case claimed this event
|
||||
tcd.handle(param1)
|
||||
elseif event == "peripheral_detach" then
|
||||
-- peripheral disconnect
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if device == plc_dev.reactor then
|
||||
println_ts("reactor disconnected!")
|
||||
log.error("reactor logic adapter disconnected")
|
||||
|
||||
plc_state.no_reactor = true
|
||||
plc_state.degraded = true
|
||||
elseif networked and type == "modem" then
|
||||
---@cast device Modem
|
||||
-- we only care if this is our wireless modem
|
||||
-- note, check init_ok first since nic will be nil if it is false
|
||||
if plc_state.init_ok and nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
|
||||
println_ts("comms modem disconnected!")
|
||||
log.warning("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log.info("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
plc_state.no_modem = true
|
||||
plc_state.degraded = 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
|
||||
end
|
||||
else
|
||||
log.warning("a modem was disconnected")
|
||||
end
|
||||
end
|
||||
-- pass this timer event onto the right handler
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
loop_tick()
|
||||
elseif networked and conn_watchdog.is_timer(param1) then
|
||||
-- supervisor connection timed out
|
||||
plc_comms.close()
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
|
||||
else
|
||||
-- notify timer callback dispatcher, no other handler claimed this event
|
||||
tcd.handle(param1)
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "peripheral" then
|
||||
-- peripheral connect
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if plc_state.no_reactor and (type == "fissionReactorLogicAdapter") then
|
||||
-- reconnected reactor
|
||||
plc_dev.reactor = device
|
||||
plc_state.no_reactor = false
|
||||
|
||||
println_ts("reactor reconnected.")
|
||||
log.info("reactor reconnected")
|
||||
|
||||
-- we need to assume formed here as we cannot check in this main loop
|
||||
-- RPS will identify if it isn't and this will get set false later
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked or not plc_state.no_modem) and plc_state.reactor_formed then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
if plc_state.init_ok then
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
rps.reconnect_reactor(plc_dev.reactor)
|
||||
if networked then
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed/reconnected
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
end
|
||||
elseif networked and type == "modem" then
|
||||
---@cast device Modem
|
||||
-- note, check init_ok first since nic will be nil if it is false
|
||||
if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
|
||||
-- reconnected modem
|
||||
plc_dev.modem = device
|
||||
plc_state.no_modem = false
|
||||
|
||||
if plc_state.init_ok then nic.connect(device) end
|
||||
|
||||
println_ts("wireless modem reconnected.")
|
||||
log.info("comms modem reconnected")
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if not plc_state.no_reactor then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
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
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "clock_start" then
|
||||
-- start loop clock
|
||||
loop_clock.start()
|
||||
log.debug("main thread clock started")
|
||||
elseif event == "peripheral" then
|
||||
-- peripheral connect
|
||||
local type, device = ppm.mount(param1)
|
||||
if type ~= nil and device ~= nil then
|
||||
backplane.attach(param1, type, device, println_ts)
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "peripheral_detach" then
|
||||
-- peripheral disconnect
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
if type ~= nil and device ~= nil then
|
||||
backplane.detach(param1, type, device, println_ts)
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
log.info("terminate requested, main thread exiting")
|
||||
log.info("OS: terminate requested, main thread exiting")
|
||||
-- rps handles reactor shutdown
|
||||
plc_state.shutdown = true
|
||||
break
|
||||
@@ -277,8 +189,7 @@ function threads.thread__main(smem, init)
|
||||
-- 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...")
|
||||
util.push_event("clock_start")
|
||||
log.info("OS: main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -299,66 +210,52 @@ function threads.thread__rps(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("rps", true)
|
||||
log.debug("rps thread start")
|
||||
log.debug("OS: 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 = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
|
||||
local rps_queue = smem.q.mq_rps
|
||||
|
||||
local MQ__RPS_CMD = smem.q_types.MQ__RPS_CMD
|
||||
|
||||
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
|
||||
was_linked = true
|
||||
-- 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 was_linked = true end
|
||||
|
||||
if (not plc_state.no_reactor) and rps.is_formed() then
|
||||
-- check reactor status
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local reactor_status = reactor.getStatus()
|
||||
databus.tx_reactor_state(reactor_status)
|
||||
-- check reactor status
|
||||
if (not plc_state.no_reactor) and rps.is_formed() then
|
||||
local active = rps.check_active()
|
||||
|
||||
-- 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)
|
||||
if rps.is_tripped() and reactor_status then
|
||||
rps.scram()
|
||||
end
|
||||
end
|
||||
databus.tx_reactor_state(active)
|
||||
|
||||
-- if we are in standalone mode and the front panel isn't working, 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 or smem.plc_state.fp_ok) then rps.reset(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)
|
||||
if rps.is_tripped() and active then rps.scram() end
|
||||
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 we are in standalone mode and the front panel isn't working, 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 or smem.plc_state.fp_ok) then rps.reset(true) end
|
||||
|
||||
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
|
||||
-- check safety (SCRAM occurs if tripped)
|
||||
local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor)
|
||||
if rps_tripped and rps_first then
|
||||
println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")")
|
||||
if networked then plc_comms.send_rps_alarm(rps_status_string) end
|
||||
end
|
||||
|
||||
-- check for messages in the message queue
|
||||
@@ -368,24 +265,23 @@ function threads.thread__rps(smem)
|
||||
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
|
||||
if msg.message == MQ__RPS_CMD.SCRAM then
|
||||
-- SCRAM
|
||||
log.info("RPS: OS requested SCRAM")
|
||||
rps.scram()
|
||||
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
|
||||
-- lost peripheral(s)
|
||||
log.info("RPS: received PLC degraded alert")
|
||||
rps.trip_fault()
|
||||
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
|
||||
-- watchdog tripped
|
||||
println_ts("RPS: supervisor timeout")
|
||||
log.warning("RPS: received supervisor timeout alert")
|
||||
rps.trip_timeout()
|
||||
elseif msg.message == MQ__RPS_CMD.RESET_REATTACH then
|
||||
-- reset on reactor re-connect
|
||||
rps.reset_reattach()
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
@@ -396,17 +292,17 @@ function threads.thread__rps(smem)
|
||||
-- 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
|
||||
log.info("OS: rps thread shutdown initiated")
|
||||
|
||||
if rps.scram() then
|
||||
println_ts("exiting, reactor disabled")
|
||||
log.info("OS: rps thread reactor SCRAM OK on exit")
|
||||
else
|
||||
println_ts("exiting, reactor failed to disable")
|
||||
log.error("OS: rps thread failed to SCRAM reactor on exit")
|
||||
end
|
||||
log.info("rps thread exiting")
|
||||
|
||||
log.info("OS: rps thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@@ -428,8 +324,8 @@ function threads.thread__rps(smem)
|
||||
databus.tx_rt_status("rps", false)
|
||||
|
||||
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...")
|
||||
smem.plc_sys.rps.scram()
|
||||
log.info("OS: rps thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@@ -448,24 +344,24 @@ function threads.thread__comms_tx(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms_tx", true)
|
||||
log.debug("comms tx thread start")
|
||||
log.debug("OS: comms tx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
local comms_queue = smem.q.mq_comms_tx
|
||||
local plc_state = smem.plc_state
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
local comms_queue = smem.q.mq_comms_tx
|
||||
|
||||
local MQ__COMM_CMD = smem.q_types.MQ__COMM_CMD
|
||||
|
||||
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 ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if msg.message == MQ__COMM_CMD.SEND_STATUS then
|
||||
@@ -473,10 +369,6 @@ function threads.thread__comms_tx(smem)
|
||||
plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
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
|
||||
|
||||
@@ -486,7 +378,7 @@ function threads.thread__comms_tx(smem)
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("comms tx thread exiting")
|
||||
log.info("OS: comms tx thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@@ -508,7 +400,7 @@ function threads.thread__comms_tx(smem)
|
||||
databus.tx_rt_status("comms_tx", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms tx thread restarting in 5 seconds...")
|
||||
log.info("OS: comms tx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@@ -521,41 +413,39 @@ end
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__comms_rx(smem)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
|
||||
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms_rx", true)
|
||||
log.debug("comms rx thread start")
|
||||
log.debug("OS: comms rx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
local setpoints = smem.setpoints
|
||||
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
|
||||
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
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.NETWORK 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, plc_state, setpoints)
|
||||
plc_comms.handle_packet(msg.message, plc_state, setpoints, println_ts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -565,7 +455,7 @@ function threads.thread__comms_rx(smem)
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("comms rx thread exiting")
|
||||
log.info("OS: comms rx thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@@ -587,7 +477,7 @@ function threads.thread__comms_rx(smem)
|
||||
databus.tx_rt_status("comms_rx", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms rx thread restarting in 5 seconds...")
|
||||
log.info("OS: comms rx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@@ -606,12 +496,13 @@ function threads.thread__setpoint_control(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("spctl", true)
|
||||
log.debug("setpoint control thread start")
|
||||
log.debug("OS: setpoint control thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
local setpoints = smem.setpoints
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_dev = smem.plc_dev
|
||||
local setpoints = smem.setpoints
|
||||
|
||||
local last_update = util.time()
|
||||
local running = false
|
||||
@@ -624,14 +515,10 @@ function threads.thread__setpoint_control(smem)
|
||||
|
||||
-- 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
|
||||
---@cast reactor table won't be nil
|
||||
|
||||
if not plc_state.no_reactor then
|
||||
-- check if we should start ramping
|
||||
if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then
|
||||
local cur_burn_rate = reactor.getBurnRate()
|
||||
@@ -698,7 +585,7 @@ function threads.thread__setpoint_control(smem)
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("setpoint control thread exiting")
|
||||
log.info("OS: setpoint control thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@@ -720,7 +607,7 @@ function threads.thread__setpoint_control(smem)
|
||||
databus.tx_rt_status("spctl", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("setpoint control thread restarting in 5 seconds...")
|
||||
log.info("OS: setpoint control thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
|
||||
299
rtu/backplane.lua
Normal file
299
rtu/backplane.lua
Normal file
@@ -0,0 +1,299 @@
|
||||
--
|
||||
-- RTU Gateway System Core Peripheral Backplane
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("rtu.databus")
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local println = util.println
|
||||
|
||||
---@class rtu_backplane
|
||||
local backplane = {}
|
||||
|
||||
local _bp = {
|
||||
smem = nil, ---@type rtu_shared_memory
|
||||
|
||||
wlan_pref = true,
|
||||
lan_iface = "",
|
||||
|
||||
act_nic = nil, ---@type nic
|
||||
wd_nic = nil, ---@type nic|nil
|
||||
wl_nic = nil, ---@type nic|nil
|
||||
nic_map = {}, ---@type nic[] connected nics
|
||||
|
||||
sounders = {} ---@type rtu_speaker_sounder[]
|
||||
}
|
||||
|
||||
-- network interfaces indexed by peripheral names
|
||||
backplane.nics = _bp.nic_map
|
||||
|
||||
-- initialize the system peripheral backplane
|
||||
---@param config rtu_config
|
||||
---@param __shared_memory rtu_shared_memory
|
||||
---@return boolean success
|
||||
function backplane.init(config, __shared_memory)
|
||||
_bp.smem = __shared_memory
|
||||
_bp.wlan_pref = config.PreferWireless
|
||||
_bp.lan_iface = config.WiredModem
|
||||
|
||||
-- Modem Init
|
||||
|
||||
-- init wired NIC
|
||||
if type(_bp.lan_iface) == "string" then
|
||||
local modem = ppm.get_modem(_bp.lan_iface)
|
||||
local wd_nic = network.nic(modem, config.SVR_Channel)
|
||||
|
||||
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
|
||||
|
||||
_bp.wd_nic = wd_nic
|
||||
_bp.act_nic = wd_nic -- set this as active for now
|
||||
_bp.nic_map[_bp.lan_iface] = wd_nic
|
||||
|
||||
wd_nic.closeAll()
|
||||
wd_nic.open(config.RTU_Channel)
|
||||
|
||||
databus.tx_hw_wd_modem(modem ~= nil)
|
||||
end
|
||||
|
||||
-- init wireless NIC(s)
|
||||
if config.WirelessModem then
|
||||
local modem, iface = ppm.get_wireless_modem()
|
||||
local wl_nic = network.nic(modem, config.SVR_Channel)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
|
||||
|
||||
-- set this as active if connected or if both modems are disconnected and this is preferred
|
||||
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
|
||||
_bp.act_nic = wl_nic
|
||||
end
|
||||
|
||||
_bp.wl_nic = wl_nic
|
||||
if iface then _bp.nic_map[iface] = wl_nic end
|
||||
|
||||
wl_nic.closeAll()
|
||||
wl_nic.open(config.RTU_Channel)
|
||||
|
||||
databus.tx_hw_wl_modem(modem ~= nil)
|
||||
end
|
||||
|
||||
-- at least one comms modem is required
|
||||
if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then
|
||||
println("startup> no comms modem found")
|
||||
log.warning("BKPLN: no comms modem on startup")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Speaker Init
|
||||
|
||||
-- find and setup all speakers
|
||||
local speakers = ppm.get_all_devices("speaker")
|
||||
for _, s in pairs(speakers) do
|
||||
log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(s))
|
||||
|
||||
local sounder = rtu.init_sounder(s)
|
||||
table.insert(_bp.sounders, sounder)
|
||||
|
||||
log.debug(util.c("BKPLN: added speaker sounder, attached as ", sounder.name))
|
||||
end
|
||||
|
||||
databus.tx_hw_spkr_count(#_bp.sounders)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- get the active NIC
|
||||
function backplane.active_nic() return _bp.act_nic end
|
||||
|
||||
-- get the standby NIC
|
||||
---@return nic|nil
|
||||
function backplane.standby_nic() return util.trinary(_bp.act_nic == _bp.wl_nic, _bp.wd_nic, _bp.wl_nic) end
|
||||
|
||||
-- get the sounder interfaces
|
||||
function backplane.sounders() return _bp.sounders end
|
||||
|
||||
-- periodic backplane peripheral tasks
|
||||
function backplane.periodic()
|
||||
if _bp.wd_nic then databus.tx_wd_net(_bp.wd_nic.periodic()) end
|
||||
if _bp.wl_nic then databus.tx_wl_net(_bp.wl_nic.periodic()) end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral attach
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param iface string
|
||||
---@param print_no_fp function
|
||||
function backplane.attach(type, device, iface, print_no_fp)
|
||||
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
|
||||
|
||||
local comms = _bp.smem.rtu_sys.rtu_comms
|
||||
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
local m_is_wl = device.isWireless()
|
||||
|
||||
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
|
||||
|
||||
if wd_nic and (_bp.lan_iface == iface) then
|
||||
-- connect this as the wired NIC
|
||||
wd_nic.connect(device)
|
||||
_bp.nic_map[iface] = wd_nic
|
||||
|
||||
log.info("BKPLN: WIRED PHY_UP " .. iface)
|
||||
print_no_fp("wired comms modem reconnected")
|
||||
|
||||
databus.tx_hw_wd_modem(true)
|
||||
|
||||
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
|
||||
-- switch back to preferred wired
|
||||
_bp.act_nic = wd_nic
|
||||
|
||||
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
|
||||
log.info("BKPLN: switched comms to wired modem (preferred)")
|
||||
end
|
||||
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
|
||||
-- connect this as the wireless NIC
|
||||
wl_nic.connect(device)
|
||||
_bp.nic_map[iface] = wl_nic
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
|
||||
print_no_fp("wireless comms modem reconnected")
|
||||
|
||||
databus.tx_hw_wl_modem(true)
|
||||
|
||||
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
|
||||
-- switch back to preferred wireless
|
||||
_bp.act_nic = wl_nic
|
||||
|
||||
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
|
||||
log.info("BKPLN: switched comms to wireless modem (preferred)")
|
||||
end
|
||||
elseif wl_nic and m_is_wl then
|
||||
-- the wireless NIC already has a modem
|
||||
device.closeAll()
|
||||
|
||||
print_no_fp("standby wireless modem connected")
|
||||
log.info("BKPLN: standby wireless modem connected")
|
||||
else
|
||||
device.closeAll()
|
||||
|
||||
print_no_fp("unassigned modem connected")
|
||||
log.warning("BKPLN: unassigned modem connected")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
|
||||
log.info("BKPLN: SPEAKER LINK_UP " .. iface)
|
||||
|
||||
table.insert(_bp.sounders, rtu.init_sounder(device))
|
||||
|
||||
print_no_fp("a speaker was connected")
|
||||
log.info("BKPLN: setup speaker sounder for speaker " .. iface)
|
||||
|
||||
databus.tx_hw_spkr_count(#_bp.sounders)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral detach
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param iface string
|
||||
---@param print_no_fp function
|
||||
function backplane.detach(type, device, iface, print_no_fp)
|
||||
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
|
||||
|
||||
local comms = _bp.smem.rtu_sys.rtu_comms
|
||||
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
log.info(util.c("BKPLN: PHY_DETACH ", iface))
|
||||
|
||||
_bp.nic_map[iface] = nil
|
||||
|
||||
if wd_nic and wd_nic.is_modem(device) then
|
||||
wd_nic.disconnect()
|
||||
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
|
||||
|
||||
databus.tx_hw_wd_modem(false)
|
||||
elseif wl_nic and wl_nic.is_modem(device) then
|
||||
wl_nic.disconnect()
|
||||
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
|
||||
|
||||
databus.tx_hw_wl_modem(false)
|
||||
end
|
||||
|
||||
-- we only care if this is our active comms modem
|
||||
if _bp.act_nic.is_modem(device) then
|
||||
print_no_fp("active comms modem disconnected")
|
||||
log.warning("BKPLN: active comms modem disconnected")
|
||||
|
||||
-- failover and try to find a new comms modem
|
||||
if _bp.act_nic == wl_nic then
|
||||
-- wireless active disconnected
|
||||
-- try to find another wireless modem, otherwise switch to wired
|
||||
local modem, m_iface = ppm.get_wireless_modem()
|
||||
if wl_nic and modem then
|
||||
log.info("BKPLN: found another wireless modem, using it for comms")
|
||||
|
||||
wl_nic.connect(modem)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
|
||||
|
||||
databus.tx_hw_wl_modem(true)
|
||||
elseif wd_nic and wd_nic.is_connected() then
|
||||
_bp.act_nic = wd_nic
|
||||
|
||||
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
|
||||
log.info("BKPLN: switched comms to wired modem")
|
||||
else
|
||||
-- no other wireless modems, wired unavailable
|
||||
comms.close(_bp.smem.rtu_state)
|
||||
end
|
||||
elseif wl_nic and wl_nic.is_connected() then
|
||||
-- wired active disconnected, wireless available
|
||||
_bp.act_nic = wl_nic
|
||||
|
||||
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
|
||||
log.info("BKPLN: switched comms to wireless modem")
|
||||
else
|
||||
-- wired active disconnected, wireless unavailable
|
||||
comms.close(_bp.smem.rtu_state)
|
||||
end
|
||||
elseif wd_nic and wd_nic.is_modem(device) then
|
||||
-- wired, but not active
|
||||
print_no_fp("standby wired modem disconnected")
|
||||
log.info("BKPLN: standby wired modem disconnected")
|
||||
elseif wl_nic and wl_nic.is_modem(device) then
|
||||
-- wireless, but not active
|
||||
print_no_fp("standby wireless modem disconnected")
|
||||
log.info("BKPLN: standby wireless modem disconnected")
|
||||
else
|
||||
print_no_fp("unassigned modem disconnected")
|
||||
log.warning("BKPLN: unassigned modem disconnected")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
|
||||
log.info("BKPLN: SPEAKER LINK_DOWN " .. iface)
|
||||
|
||||
for i = 1, #_bp.sounders do
|
||||
if _bp.sounders[i].speaker == device then
|
||||
table.remove(_bp.sounders, i)
|
||||
|
||||
print_no_fp("a speaker was disconnected")
|
||||
log.warning("BKPLN: speaker sounder " .. iface .. " disconnected")
|
||||
|
||||
databus.tx_hw_spkr_count(#_bp.sounders)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return backplane
|
||||
@@ -27,13 +27,17 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
local self = {
|
||||
checking_wl = true,
|
||||
wd_modem = nil, ---@type Modem|nil
|
||||
wl_modem = nil, ---@type Modem|nil
|
||||
|
||||
nic = nil, ---@type nic
|
||||
net_listen = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = util.time_ms() * 10,
|
||||
|
||||
self_check_pass = true,
|
||||
|
||||
self_check_wireless = true,
|
||||
|
||||
settings = nil, ---@type rtu_config
|
||||
|
||||
run_test_btn = nil, ---@type PushButton
|
||||
@@ -55,18 +59,16 @@ end
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function send_sv(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(comms.BROADCAST, util.time_ms() * 10, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
self.nic.transmit(self.settings.SVR_Channel, self.settings.RTU_Channel, s_pkt)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
self.nic.transmit(self.settings.SVR_Channel, self.settings.RTU_Channel, frame)
|
||||
end
|
||||
|
||||
-- handle an establish message from the supervisor
|
||||
---@param packet mgmt_frame
|
||||
---@param packet mgmt_packet
|
||||
local function handle_packet(packet)
|
||||
local error_msg = nil
|
||||
|
||||
@@ -78,10 +80,7 @@ local function handle_packet(packet)
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack== ESTABLISH_ACK.ALLOW then
|
||||
self.self_check_msg(nil, true, "")
|
||||
self.sv_addr = packet.scada_frame.src_addr()
|
||||
send_sv(MGMT_TYPE.CLOSE, {})
|
||||
if self.self_check_pass then check_complete() end
|
||||
-- OK
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
error_msg = "error: supervisor connection denied"
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
@@ -98,18 +97,20 @@ local function handle_packet(packet)
|
||||
end
|
||||
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
|
||||
if error_msg then
|
||||
self.self_check_msg(nil, false, error_msg)
|
||||
else
|
||||
self.self_check_msg(nil, true, "")
|
||||
end
|
||||
|
||||
util.push_event("conn_test_complete", error_msg == nil)
|
||||
end
|
||||
|
||||
-- handle supervisor connection failure
|
||||
local function handle_timeout()
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
|
||||
util.push_event("conn_test_complete", false)
|
||||
end
|
||||
|
||||
|
||||
@@ -129,10 +130,18 @@ local function self_check()
|
||||
self.self_check_pass = true
|
||||
|
||||
local cfg = self.settings
|
||||
local modem = ppm.get_wireless_modem()
|
||||
self.wd_modem = ppm.get_modem(cfg.WiredModem)
|
||||
self.wl_modem = ppm.get_wireless_modem()
|
||||
local valid_cfg = rtu.validate_config(cfg)
|
||||
|
||||
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway")
|
||||
if cfg.WiredModem then
|
||||
self.self_check_msg("> check wired comms modem connected...", self.wd_modem, "please connect the wired comms modem " .. cfg.WiredModem)
|
||||
end
|
||||
|
||||
if cfg.WirelessModem then
|
||||
self.self_check_msg("> check wireless/ender modem connected...", self.wl_modem, "please connect an ender or wireless modem for wireless comms")
|
||||
end
|
||||
|
||||
self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones")
|
||||
|
||||
-- check redstone configurations
|
||||
@@ -211,27 +220,37 @@ local function self_check()
|
||||
end
|
||||
end
|
||||
|
||||
if valid_cfg and modem then
|
||||
self.self_check_msg("> check supervisor connection...")
|
||||
if valid_cfg then
|
||||
self.checking_wl = true
|
||||
|
||||
-- init mac as needed
|
||||
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
|
||||
network.init_mac(cfg.AuthKey)
|
||||
if cfg.WirelessModem and self.wl_modem then
|
||||
self.self_check_msg("> check wireless supervisor connection...")
|
||||
|
||||
-- init mac as needed
|
||||
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
|
||||
network.init_mac(cfg.AuthKey)
|
||||
else
|
||||
network.deinit_mac()
|
||||
end
|
||||
|
||||
comms.set_trusted_range(cfg.TrustedRange)
|
||||
|
||||
self.nic = network.nic(self.wl_modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(cfg.RTU_Channel)
|
||||
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.RTU, {} })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
elseif cfg.WiredModem and self.wd_modem then
|
||||
-- skip to wired
|
||||
util.push_event("conn_test_complete", true)
|
||||
else
|
||||
network.deinit_mac()
|
||||
self.self_check_msg("> no modem, can't test supervisor connection", false)
|
||||
end
|
||||
|
||||
self.nic = network.nic(modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(cfg.RTU_Channel)
|
||||
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.RTU, {} })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
else
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
@@ -266,9 +285,9 @@ function check.create(main_pane, settings_cfg, check_sys, style)
|
||||
|
||||
local sc = Div{parent=check_sys,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=check_sys,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg}
|
||||
|
||||
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
self.sc_log = ListBox{parent=sc,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local last_check = { nil, nil }
|
||||
|
||||
@@ -291,7 +310,7 @@ function check.create(main_pane, settings_cfg, check_sys, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sc,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
end
|
||||
|
||||
@@ -303,16 +322,56 @@ end
|
||||
---@param distance integer
|
||||
function check.receive_sv(side, sender, reply_to, message, distance)
|
||||
if self.nic ~= nil and self.net_listen then
|
||||
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
local frame = self.nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
if frame and frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local pkt = comms.mgmt_container().decode(frame)
|
||||
if pkt then
|
||||
tcd.abort(handle_timeout)
|
||||
handle_packet(mgmt_pkt.get())
|
||||
handle_packet(pkt)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle completed connection tests
|
||||
---@param pass boolean
|
||||
function check.conn_test_callback(pass)
|
||||
local cfg = self.settings
|
||||
|
||||
if self.checking_wl then
|
||||
if not pass then
|
||||
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wireless interface, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
|
||||
end
|
||||
|
||||
if cfg.WiredModem and self.wd_modem then
|
||||
self.checking_wl = false
|
||||
self.self_check_msg("> check wired supervisor connection...")
|
||||
|
||||
comms.set_trusted_range(0)
|
||||
|
||||
self.nic = network.nic(self.wd_modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(cfg.RTU_Channel)
|
||||
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.RTU, {} })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
else
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
end
|
||||
else
|
||||
if not pass then
|
||||
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wired interface, the wire is intact, and your channels are correct")
|
||||
end
|
||||
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
end
|
||||
end
|
||||
|
||||
return check
|
||||
|
||||
@@ -72,11 +72,11 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
local peri_c_6 = Div{parent=peri_cfg,x=2,y=4,width=49}
|
||||
local peri_c_7 = Div{parent=peri_cfg,x=2,y=4,width=49}
|
||||
|
||||
local peri_pane = MultiPane{parent=peri_cfg,x=1,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}}
|
||||
local peri_pane = MultiPane{parent=peri_cfg,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}}
|
||||
|
||||
TextBox{parent=peri_cfg,x=1,y=2,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)}
|
||||
TextBox{parent=peri_cfg,y=2,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)}
|
||||
|
||||
local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local peri_list = ListBox{parent=peri_c_1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function peri_revert()
|
||||
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
|
||||
@@ -99,22 +99,22 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=peri_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
local peri_revert_btn = PushButton{parent=peri_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=peri_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=peri_c_1,x=35,y=14,min_width=7,text="Add +",callback=function()peri_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
local peri_apply_btn = PushButton{parent=peri_c_1,x=43,y=14,min_width=7,text="Apply",callback=peri_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
TextBox{parent=peri_c_2,x=1,y=1,text="Select one of the below devices to use."}
|
||||
TextBox{parent=peri_c_2,y=1,text="Select one of the below devices to use."}
|
||||
|
||||
self.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
self.ppm_devs = ListBox{parent=peri_c_2,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
PushButton{parent=peri_c_2,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_2,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_2,x=26,y=14,min_width=24,text="I don't see my device!",callback=function()peri_pane.set_value(7)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=peri_c_7,x=1,y=1,height=10,text="Make sure your device is either touching the RTU or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
|
||||
TextBox{parent=peri_c_7,x=1,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."}
|
||||
PushButton{parent=peri_c_7,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=peri_c_7,y=1,height=10,text="Make sure your device is either touching the RTU or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
|
||||
TextBox{parent=peri_c_7,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."}
|
||||
PushButton{parent=peri_c_7,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local new_peri_attrs = { "", "" }
|
||||
local function new_peri(name, type)
|
||||
@@ -201,7 +201,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
|
||||
---@cast entry ppm_entry
|
||||
local line = Div{parent=self.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)}
|
||||
PushButton{parent=line,x=1,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
PushButton{parent=line,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
TextBox{parent=line,x=11,y=1,text=name,fg_bg=cpair(colors.black,bkg)}
|
||||
TextBox{parent=line,x=11,y=2,text=entry.type,fg_bg=cpair(colors.gray,bkg)}
|
||||
|
||||
@@ -212,10 +212,10 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
|
||||
tool_ctl.update_peri_list()
|
||||
|
||||
TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you just can't see your device, click 'I don't see my device!' instead."}
|
||||
TextBox{parent=peri_c_3,x=1,y=5,height=4,text="Peripheral Name"}
|
||||
local p_name = TextField{parent=peri_c_3,x=1,y=6,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
|
||||
local p_type = Radio2D{parent=peri_c_3,x=1,y=8,rows=5,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
|
||||
TextBox{parent=peri_c_3,y=1,height=4,text="This feature is intended for advanced users. If you just can't see your device, click 'I don't see my device!' instead."}
|
||||
TextBox{parent=peri_c_3,y=5,height=4,text="Peripheral Name"}
|
||||
local p_name = TextField{parent=peri_c_3,y=6,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
|
||||
local p_type = Radio2D{parent=peri_c_3,y=8,rows=5,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
|
||||
local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
man_p_err.hide(true)
|
||||
|
||||
@@ -228,13 +228,13 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
else man_p_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=peri_c_3,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_3,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_3,x=44,y=14,text="Next \x1a",callback=submit_manual_peri,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
self.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""}
|
||||
self.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""}
|
||||
self.p_name_msg = TextBox{parent=peri_c_4,y=1,height=2,text=""}
|
||||
self.p_prompt = TextBox{parent=peri_c_4,y=4,height=2,text=""}
|
||||
self.p_idx = NumberField{parent=peri_c_4,x=31,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
self.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)self.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
|
||||
self.p_assign_btn = RadioButton{parent=peri_c_4,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)self.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
|
||||
|
||||
self.p_unit = NumberField{parent=peri_c_4,x=23,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
self.p_unit.disable()
|
||||
@@ -252,8 +252,8 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
end
|
||||
end
|
||||
|
||||
self.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text="",fg_bg=g_lg_fg_bg}
|
||||
self.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text="",fg_bg=g_lg_fg_bg}
|
||||
self.p_desc = TextBox{parent=peri_c_4,y=7,height=6,text="",fg_bg=g_lg_fg_bg}
|
||||
self.p_desc_ext = TextBox{parent=peri_c_4,y=6,height=7,text="",fg_bg=g_lg_fg_bg}
|
||||
|
||||
self.p_err = TextBox{parent=peri_c_4,x=8,y=14,width=32,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
self.p_err.hide(true)
|
||||
@@ -337,15 +337,15 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
self.p_idx.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=peri_c_4,x=1,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_4,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_peri_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=peri_c_5,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=peri_c_5,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=peri_c_5,y=1,text="Settings saved!"}
|
||||
PushButton{parent=peri_c_5,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=peri_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=peri_c_6,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=peri_c_6,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=peri_c_6,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=peri_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -405,9 +405,9 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
|
||||
end
|
||||
|
||||
local entry = Div{parent=peri_list,height=3}
|
||||
TextBox{parent=entry,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=entry,x=1,y=2,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=entry,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=entry,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=entry,y=2,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=entry,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
|
||||
@@ -168,14 +168,14 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
local rs_c_9 = Div{parent=rs_cfg,x=2,y=4,width=49}
|
||||
local rs_c_10 = Div{parent=rs_cfg,x=2,y=4,width=49}
|
||||
|
||||
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8,rs_c_9,rs_c_10}}
|
||||
local rs_pane = MultiPane{parent=rs_cfg,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8,rs_c_9,rs_c_10}}
|
||||
|
||||
local header = TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
|
||||
local header = TextBox{parent=rs_cfg,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
|
||||
|
||||
--#region Interface Selection
|
||||
|
||||
TextBox{parent=rs_c_1,x=1,y=1,text="Configure this computer or a redstone relay."}
|
||||
local iface_list = ListBox{parent=rs_c_1,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
TextBox{parent=rs_c_1,y=1,text="Configure this computer or a redstone relay."}
|
||||
local iface_list = ListBox{parent=rs_c_1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
-- update relay interface list
|
||||
function tool_ctl.update_relay_list()
|
||||
@@ -209,7 +209,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
end
|
||||
|
||||
local line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,x=1,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,x=3,y=2,text="This Computer",fg_bg=cpair(colors.gray,colors.white)}
|
||||
local count = #redstone_subset(ini_cfg.Redstone, nil)
|
||||
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
|
||||
@@ -220,7 +220,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
local name = relays[i]
|
||||
|
||||
line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,x=1,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,x=3,y=2,text="Redstone Relay",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=line,x=18,y=2,text=tri(mounts[name],"ONLINE","OFFLINE"),fg_bg=cpair(tri(mounts[name],colors.green,colors.red),colors.white)}
|
||||
count = #redstone_subset(ini_cfg.Redstone, name)
|
||||
@@ -232,14 +232,14 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
|
||||
tool_ctl.update_relay_list()
|
||||
|
||||
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_1,x=27,y=14,min_width=23,text="I don't see my relay!",callback=function()rs_pane.set_value(10)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Configuration List
|
||||
|
||||
TextBox{parent=rs_c_2,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
|
||||
local rs_list = ListBox{parent=rs_c_2,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
TextBox{parent=rs_c_2,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
|
||||
local rs_list = ListBox{parent=rs_c_2,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function rs_revert()
|
||||
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
|
||||
@@ -275,7 +275,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
header.set_value(" Redstone Connections")
|
||||
end
|
||||
|
||||
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_2,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
local rs_revert_btn = PushButton{parent=rs_c_2,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=rs_c_2,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
local rs_apply_btn = PushButton{parent=rs_c_2,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
@@ -283,9 +283,9 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
--#endregion
|
||||
--#region Port Selection
|
||||
|
||||
TextBox{parent=rs_c_3,x=1,y=1,text="Select one of the below ports to use."}
|
||||
TextBox{parent=rs_c_3,y=1,text="Select one of the below ports to use."}
|
||||
|
||||
local rs_ports = ListBox{parent=rs_c_3,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local rs_ports = ListBox{parent=rs_c_3,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function new_rs(port)
|
||||
self.rs_cfg_editing = false
|
||||
@@ -298,6 +298,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
self.rs_cfg_side_l.set_value("Output Side")
|
||||
self.rs_cfg_bundled.enable()
|
||||
self.rs_cfg_advanced.disable()
|
||||
|
||||
text = "You selected the ALL_WASTE shortcut."
|
||||
else
|
||||
self.rs_cfg_shortcut.hide(true)
|
||||
@@ -329,17 +330,15 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
io_type = "analog output "
|
||||
end
|
||||
|
||||
text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for "
|
||||
text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for " .. tri(PORT_DSGN[port] == 1, "a unit).", "the facility).")
|
||||
end
|
||||
|
||||
if PORT_DSGN[port] == 1 then
|
||||
text = text .. "a unit)."
|
||||
self.rs_cfg_unit_l.show()
|
||||
self.rs_cfg_unit.show()
|
||||
else
|
||||
self.rs_cfg_unit_l.hide(true)
|
||||
self.rs_cfg_unit.hide(true)
|
||||
text = text .. "the facility)."
|
||||
end
|
||||
if PORT_DSGN[port] == 1 then
|
||||
self.rs_cfg_unit_l.show()
|
||||
self.rs_cfg_unit.show()
|
||||
else
|
||||
self.rs_cfg_unit_l.hide(true)
|
||||
self.rs_cfg_unit.hide(true)
|
||||
end
|
||||
|
||||
self.rs_cfg_selection.set_value(text)
|
||||
@@ -349,7 +348,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
|
||||
-- add entries to redstone option list
|
||||
local all_w_macro = Div{parent=rs_ports,height=1}
|
||||
PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
PushButton{parent=all_w_macro,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
TextBox{parent=all_w_macro,x=16,y=1,width=5,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=all_w_macro,x=22,y=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)}
|
||||
|
||||
@@ -360,22 +359,22 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue)
|
||||
|
||||
local entry = Div{parent=rs_ports,height=1}
|
||||
PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
PushButton{parent=entry,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
TextBox{parent=entry,x=16,y=1,width=5,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
|
||||
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_3,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Port Configuration
|
||||
|
||||
self.rs_cfg_selection = TextBox{parent=rs_c_4,x=1,y=1,height=2,text=""}
|
||||
self.rs_cfg_selection = TextBox{parent=rs_c_4,y=1,height=2,text=""}
|
||||
|
||||
PushButton{parent=rs_c_4,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
self.rs_cfg_side_l = TextBox{parent=rs_c_4,x=1,y=4,width=11,text="Output Side"}
|
||||
local side = Radio2D{parent=rs_c_4,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
|
||||
self.rs_cfg_side_l = TextBox{parent=rs_c_4,y=4,width=11,text="Output Side"}
|
||||
local side = Radio2D{parent=rs_c_4,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
|
||||
|
||||
self.rs_cfg_unit_l = TextBox{parent=rs_c_4,x=25,y=7,width=7,text="Unit ID"}
|
||||
self.rs_cfg_unit = NumberField{parent=rs_c_4,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
@@ -384,11 +383,11 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
|
||||
end
|
||||
|
||||
self.rs_cfg_shortcut = TextBox{parent=rs_c_4,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
|
||||
self.rs_cfg_shortcut = TextBox{parent=rs_c_4,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
|
||||
self.rs_cfg_shortcut.hide(true)
|
||||
|
||||
self.rs_cfg_bundled = Checkbox{parent=rs_c_4,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
|
||||
self.rs_cfg_color = Radio2D{parent=rs_c_4,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
self.rs_cfg_bundled = Checkbox{parent=rs_c_4,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
|
||||
self.rs_cfg_color = Radio2D{parent=rs_c_4,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
self.rs_cfg_color.disable()
|
||||
|
||||
local rs_err = TextBox{parent=rs_c_4,x=8,y=14,width=30,text="Unit ID invalid.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
@@ -461,35 +460,35 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
else rs_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_4,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.rs_cfg_advanced = PushButton{parent=rs_c_4,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(9)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=rs_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
TextBox{parent=rs_c_5,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=rs_c_5,y=1,text="Settings saved!"}
|
||||
PushButton{parent=rs_c_5,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=rs_c_6,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=rs_c_6,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=rs_c_7,x=1,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."}
|
||||
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=rs_c_7,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."}
|
||||
PushButton{parent=rs_c_7,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=rs_c_8,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
|
||||
TextBox{parent=rs_c_8,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
|
||||
TextBox{parent=rs_c_8,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
|
||||
PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=rs_c_8,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
|
||||
TextBox{parent=rs_c_8,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
|
||||
TextBox{parent=rs_c_8,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
|
||||
PushButton{parent=rs_c_8,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=rs_c_9,x=1,y=1,height=5,text="Advanced Options"}
|
||||
self.rs_cfg_inverted = Checkbox{parent=rs_c_9,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=rs_c_9,y=1,height=5,text="Advanced Options"}
|
||||
self.rs_cfg_inverted = Checkbox{parent=rs_c_9,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=rs_c_9,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
PushButton{parent=rs_c_9,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=rs_c_9,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=rs_c_10,x=1,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
|
||||
PushButton{parent=rs_c_10,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=rs_c_10,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
|
||||
PushButton{parent=rs_c_10,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -571,7 +570,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
|
||||
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
|
||||
|
||||
local entry = Div{parent=rs_list,height=1}
|
||||
TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)}
|
||||
TextBox{parent=entry,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)}
|
||||
TextBox{parent=entry,x=2,y=1,width=14,text=name}
|
||||
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
|
||||
|
||||
@@ -30,9 +30,13 @@ local self = {
|
||||
importing_legacy = false,
|
||||
importing_any_dc = false,
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
wireless = nil, ---@type Checkbox
|
||||
wl_pref = nil, ---@type Checkbox
|
||||
wired = nil, ---@type Checkbox
|
||||
range = nil, ---@type NumberField
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
@@ -60,14 +64,14 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
|
||||
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
|
||||
TextBox{parent=spkr_cfg,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
|
||||
|
||||
TextBox{parent=spkr_c,x=1,y=1,height=2,text="Speakers can be connected to this RTU gateway without RTU unit configuration entries."}
|
||||
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
|
||||
TextBox{parent=spkr_c,y=1,height=2,text="Speakers can be connected to this RTU gateway without RTU unit configuration entries."}
|
||||
TextBox{parent=spkr_c,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
|
||||
|
||||
local s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
|
||||
local s_vol = NumberField{parent=spkr_c,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=spkr_c,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
@@ -80,7 +84,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
else s_vol_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=spkr_c,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -90,22 +94,88 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
||||
local net_pane = MultiPane{parent=net_cfg,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
TextBox{parent=net_cfg,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,y=1,text="Please select the network interface(s)."}
|
||||
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"}
|
||||
local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local function en_dis_pref()
|
||||
if self.wireless.get_value() and self.wired.get_value() then
|
||||
self.wl_pref.enable()
|
||||
else
|
||||
self.wl_pref.set_value(self.wireless.get_value())
|
||||
self.wl_pref.disable()
|
||||
end
|
||||
end
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local function on_wired_change(_)
|
||||
en_dis_pref()
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
self.wireless = Checkbox{parent=net_c_1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=en_dis_pref}
|
||||
self.wl_pref = Checkbox{parent=net_c_1,x=30,y=3,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
self.wired = Checkbox{parent=net_c_1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
|
||||
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
|
||||
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
|
||||
local modem_list = ListBox{parent=net_c_1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
en_dis_pref()
|
||||
|
||||
local function submit_interfaces()
|
||||
tmp_cfg.WirelessModem = self.wireless.get_value()
|
||||
|
||||
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
|
||||
tmp_cfg.PreferWireless = self.wl_pref.get_value()
|
||||
else
|
||||
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
|
||||
self.wl_pref.set_value(tmp_cfg.PreferWireless)
|
||||
end
|
||||
|
||||
if not self.wired.get_value() then
|
||||
tmp_cfg.WiredModem = false
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
if not (self.wired.get_value() or self.wireless.get_value()) then
|
||||
modem_err.set_value("Please select a modem type.")
|
||||
modem_err.show()
|
||||
elseif self.wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
|
||||
modem_err.set_value("Please select a wired modem.")
|
||||
modem_err.show()
|
||||
else
|
||||
if tmp_cfg.WirelessModem then
|
||||
self.range.enable()
|
||||
else
|
||||
self.range.set_value(0)
|
||||
self.range.disable()
|
||||
end
|
||||
|
||||
net_pane.set_value(2)
|
||||
modem_err.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_2,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,y=8,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_2,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,y=11,text="RTU Channel"}
|
||||
local rtu_chan = NumberField{parent=net_c_2,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c = tonumber(svr_chan.get_value())
|
||||
@@ -113,7 +183,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
if svr_c ~= nil and rtu_c ~= nil then
|
||||
tmp_cfg.SVR_Channel = svr_c
|
||||
tmp_cfg.RTU_Channel = rtu_c
|
||||
net_pane.set_value(2)
|
||||
net_pane.set_value(3)
|
||||
chan_err.hide(true)
|
||||
elseif svr_c == nil then
|
||||
chan_err.set_value("Please set the supervisor channel.")
|
||||
@@ -124,54 +194,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=1,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_3,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"}
|
||||
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,y=8,text="Trusted Range (Wireless Only)"}
|
||||
self.range = NumberField{parent=net_c_3,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=net_c_3,y=10,height=4,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local n3_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_ct_tr()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
local range_val = tonumber(range.get_value())
|
||||
if timeout_val ~= nil and range_val ~= nil then
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
net_pane.set_value(3)
|
||||
p2_err.hide(true)
|
||||
elseif timeout_val == nil then
|
||||
p2_err.set_value("Please set the connection timeout.")
|
||||
p2_err.show()
|
||||
local range_val = tonumber(self.range.get_value())
|
||||
|
||||
if timeout_val == nil then
|
||||
n3_err.set_value("Please set the connection timeout.")
|
||||
n3_err.show()
|
||||
elseif tmp_cfg.WirelessModem and (range_val == nil) then
|
||||
n3_err.set_value("Please set the trusted range.")
|
||||
n3_err.show()
|
||||
else
|
||||
p2_err.set_value("Please set the trusted range.")
|
||||
p2_err.show()
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
tmp_cfg.TrustedRange = tri(tmp_cfg.WirelessModem, range_val, 0)
|
||||
|
||||
if tmp_cfg.WirelessModem then
|
||||
net_pane.set_value(4)
|
||||
else
|
||||
main_pane.set_value(4)
|
||||
tmp_cfg.AuthKey = ""
|
||||
end
|
||||
|
||||
n3_err.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
|
||||
local key, _ = TextField{parent=net_c_4,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
|
||||
|
||||
local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
@@ -182,8 +260,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -191,17 +269,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
TextBox{parent=log_cfg,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
|
||||
TextBox{parent=log_c_1,y=1,text="Please configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
TextBox{parent=log_c_1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=log_c_1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
local en_dbg = Checkbox{parent=log_c_1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
@@ -218,7 +296,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -230,17 +308,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
local clr_pane = MultiPane{parent=clr_cfg,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
TextBox{parent=clr_cfg,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=clr_c_1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_1,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
TextBox{parent=clr_c_2,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
@@ -269,8 +347,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_2,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
@@ -312,19 +390,19 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
tool_ctl.color_apply.hide(true)
|
||||
|
||||
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_3,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -339,11 +417,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
local sum_c_6 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_7 = Div{parent=summary,x=2,y=4,width=49}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4,sum_c_5,sum_c_6,sum_c_7}}
|
||||
local sum_pane = MultiPane{parent=summary,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4,sum_c_5,sum_c_6,sum_c_7}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
TextBox{parent=summary,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local setting_list = ListBox{parent=sum_c_1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_settings()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
@@ -382,10 +460,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(s_vol, ini_cfg.SpeakerVolume)
|
||||
try_set(self.wireless, ini_cfg.WirelessModem)
|
||||
try_set(self.wired, ini_cfg.WiredModem ~= false)
|
||||
try_set(self.wl_pref, ini_cfg.PreferWireless)
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(rtu_chan, ini_cfg.RTU_Channel)
|
||||
try_set(timeout, ini_cfg.ConnTimeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(self.range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
@@ -411,22 +492,22 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
else sum_pane.set_value(6) end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=function()save_and_continue(true)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.settings_confirm.hide()
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,text="The following peripherals will be imported:"}
|
||||
local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
TextBox{parent=sum_c_2,y=1,text="The following peripherals will be imported:"}
|
||||
local peri_import_list = ListBox{parent=sum_c_2,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,text="The following redstone entries will be imported:"}
|
||||
local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
TextBox{parent=sum_c_3,y=1,text="The following redstone entries will be imported:"}
|
||||
local rs_import_list = ListBox{parent=sum_c_3,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local function jump_peri_conns()
|
||||
@@ -439,30 +520,30 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
show_rs_conns()
|
||||
end
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_4,x=1,y=3,height=4,text="Remember to configure any peripherals or redstone that you have connected to this RTU gateway if you have not already done so, or if you have added, removed, or modified any of them."}
|
||||
PushButton{parent=sum_c_4,x=1,y=8,min_width=24,text="Peripheral Connections",callback=jump_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=1,y=10,min_width=22,text="Redstone Connections",callback=jump_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=sum_c_4,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_4,y=3,height=4,text="Remember to configure any peripherals or redstone that you have connected to this RTU gateway if you have not already done so, or if you have added, removed, or modified any of them."}
|
||||
PushButton{parent=sum_c_4,y=8,min_width=24,text="Peripheral Connections",callback=jump_peri_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,y=10,min_width=22,text="Redstone Connections",callback=jump_rs_conns,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_5,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
TextBox{parent=sum_c_5,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/rtu/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_5,x=1,y=14,min_width=8,text="Cancel",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_5,y=14,min_width=8,text="Cancel",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_5,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_6,x=1,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=sum_c_6,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_6,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_6,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_7,x=1,y=1,height=8,text="Warning!\n\nSome of the devices in your old config file aren't currently connected. If the device isn't connected, the options can't be properly validated. Please either connect your devices and try again or complete the import without validation on those entry's settings."}
|
||||
TextBox{parent=sum_c_7,x=1,y=10,height=3,text="Afterwards, either (a) edit then save entries for currently disconnected devices to properly configure or (b) delete those entries."}
|
||||
PushButton{parent=sum_c_7,x=1,y=14,text="\x1b Back",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=sum_c_7,y=1,height=8,text="Warning!\n\nSome of the devices in your old config file aren't currently connected. If the device isn't connected, the options can't be properly validated. Please either connect your devices and try again or complete the import without validation on those entry's settings."}
|
||||
TextBox{parent=sum_c_7,y=10,height=3,text="Afterwards, either (a) edit then save entries for currently disconnected devices to properly configure or (b) delete those entries."}
|
||||
PushButton{parent=sum_c_7,y=14,text="\x1b Back",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_7,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(1)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -558,9 +639,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
|
||||
local line = Div{parent=peri_import_list,height=3}
|
||||
TextBox{parent=line,x=1,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,x=1,y=2,text=status,fg_bg=cpair(color,colors.white)}
|
||||
TextBox{parent=line,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=line,y=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
|
||||
TextBox{parent=line,y=2,text=status,fg_bg=cpair(color,colors.white)}
|
||||
TextBox{parent=line,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
|
||||
rs_import_list.remove_all()
|
||||
@@ -579,7 +660,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
|
||||
|
||||
local line = Div{parent=rs_import_list,height=1}
|
||||
TextBox{parent=line,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=line,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=line,x=2,y=1,width=14,text=name}
|
||||
TextBox{parent=line,x=18,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=line,x=40,y=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
|
||||
@@ -656,7 +737,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
textbox = TextBox{parent=line,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
@@ -665,6 +746,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
|
||||
end
|
||||
end
|
||||
|
||||
-- generate the list of available/assigned wired modems
|
||||
function tool_ctl.gen_modem_list()
|
||||
modem_list.remove_all()
|
||||
|
||||
local enable = self.wired.get_value()
|
||||
|
||||
local function select(iface)
|
||||
tmp_cfg.WiredModem = iface
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
local modems = ppm.get_wired_modem_list()
|
||||
local missing = { tmp = true, ini = true }
|
||||
|
||||
for iface, _ in pairs(modems) do
|
||||
if ini_cfg.WiredModem == iface then missing.ini = false end
|
||||
if tmp_cfg.WiredModem == iface then missing.tmp = false end
|
||||
end
|
||||
|
||||
if missing.tmp and tmp_cfg.WiredModem then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
|
||||
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
|
||||
end
|
||||
|
||||
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
|
||||
-- list wired modems
|
||||
for iface, _ in pairs(modems) do
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == iface
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text=iface}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ local changes = {
|
||||
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
|
||||
{ "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } },
|
||||
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } },
|
||||
{ "v1.12.0", { "Added support for redstone relays" } }
|
||||
{ "v1.12.0", { "Added support for redstone relays" } },
|
||||
{ "v1.13.0", { "Added support for wired communications modems" } }
|
||||
}
|
||||
|
||||
---@class rtu_configurator
|
||||
@@ -64,39 +65,44 @@ local tool_ctl = {
|
||||
viewing_config = false,
|
||||
jumped_to_color = false,
|
||||
|
||||
view_gw_cfg = nil, ---@type PushButton
|
||||
dev_cfg = nil, ---@type PushButton
|
||||
rs_cfg = nil, ---@type PushButton
|
||||
color_cfg = nil, ---@type PushButton
|
||||
color_next = nil, ---@type PushButton
|
||||
color_apply = nil, ---@type PushButton
|
||||
settings_apply = nil, ---@type PushButton
|
||||
settings_confirm = nil, ---@type PushButton
|
||||
view_gw_cfg = nil, ---@type PushButton
|
||||
dev_cfg = nil, ---@type PushButton
|
||||
rs_cfg = nil, ---@type PushButton
|
||||
color_cfg = nil, ---@type PushButton
|
||||
color_next = nil, ---@type PushButton
|
||||
color_apply = nil, ---@type PushButton
|
||||
settings_apply = nil, ---@type PushButton
|
||||
settings_confirm = nil, ---@type PushButton
|
||||
|
||||
go_home = nil, ---@type function
|
||||
gen_summary = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
update_peri_list = nil, ---@type function
|
||||
update_relay_list = nil, ---@type function
|
||||
gen_peri_summary = nil, ---@type function
|
||||
gen_rs_summary = nil, ---@type function
|
||||
go_home = nil, ---@type function
|
||||
gen_summary = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
update_peri_list = nil, ---@type function
|
||||
update_relay_list = nil, ---@type function
|
||||
gen_peri_summary = nil, ---@type function
|
||||
gen_rs_summary = nil, ---@type function
|
||||
|
||||
gen_modem_list = function () end
|
||||
}
|
||||
|
||||
---@class rtu_config
|
||||
local tmp_cfg = {
|
||||
SpeakerVolume = 1.0,
|
||||
Peripherals = {}, ---@type rtu_peri_definition[]
|
||||
Redstone = {}, ---@type rtu_rs_definition[]
|
||||
SVR_Channel = nil, ---@type integer
|
||||
RTU_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string|nil
|
||||
LogMode = 0, ---@type LOG_MODE
|
||||
Peripherals = {}, ---@type rtu_peri_definition[]
|
||||
Redstone = {}, ---@type rtu_rs_definition[]
|
||||
WirelessModem = true,
|
||||
WiredModem = false, ---@type string|false
|
||||
PreferWireless = true,
|
||||
SVR_Channel = nil, ---@type integer
|
||||
RTU_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string
|
||||
LogMode = 0, ---@type LOG_MODE
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
FrontPanelTheme = 1, ---@type FP_THEME
|
||||
ColorMode = 1 ---@type COLOR_MODE
|
||||
FrontPanelTheme = 1, ---@type FP_THEME
|
||||
ColorMode = 1 ---@type COLOR_MODE
|
||||
}
|
||||
|
||||
---@class rtu_config
|
||||
@@ -106,6 +112,9 @@ local settings_cfg = {}
|
||||
|
||||
local fields = {
|
||||
{ "SpeakerVolume", "Speaker Volume", 1.0 },
|
||||
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
|
||||
{ "WiredModem", "Wired Comms Modem", false },
|
||||
{ "PreferWireless", "Prefer Wireless Modem", true },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "RTU_Channel", "RTU Channel", 16242 },
|
||||
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||
@@ -164,20 +173,20 @@ local function config_view(display)
|
||||
|
||||
TextBox{parent=display,y=1,text="RTU Gateway Configurator",alignment=CENTER,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
local root_pane_div = Div{parent=display,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local spkr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
local peri_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local rs_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local check_sys = Div{parent=root_pane_div,x=1,y=1}
|
||||
local main_page = Div{parent=root_pane_div,y=1}
|
||||
local spkr_cfg = Div{parent=root_pane_div,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,y=1}
|
||||
local summary = Div{parent=root_pane_div,y=1}
|
||||
local changelog = Div{parent=root_pane_div,y=1}
|
||||
local peri_cfg = Div{parent=root_pane_div,y=1}
|
||||
local rs_cfg = Div{parent=root_pane_div,y=1}
|
||||
local check_sys = Div{parent=root_pane_div,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg,check_sys}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg,check_sys}}
|
||||
|
||||
--#region Main Page
|
||||
|
||||
@@ -274,20 +283,20 @@ local function config_view(display)
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=changelog,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local c_log = ListBox{parent=cl,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=cl,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -313,6 +322,9 @@ function configurator.configure(ask_config)
|
||||
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
-- set tmp_cfg so interface lists are correct
|
||||
tmp_cfg.WiredModem = ini_cfg.WiredModem
|
||||
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
|
||||
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
|
||||
|
||||
@@ -329,6 +341,8 @@ function configurator.configure(ask_config)
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
tool_ctl.gen_modem_list()
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
@@ -345,16 +359,20 @@ function configurator.configure(ask_config)
|
||||
display.handle_paste(param1)
|
||||
elseif event == "modem_message" then
|
||||
check.receive_sv(param1, param2, param3, param4, param5)
|
||||
elseif event == "conn_test_complete" then
|
||||
check.conn_test_callback(param1)
|
||||
elseif event == "peripheral_detach" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.handle_unmount(param1)
|
||||
tool_ctl.update_peri_list()
|
||||
tool_ctl.update_relay_list()
|
||||
tool_ctl.gen_modem_list()
|
||||
elseif event == "peripheral" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.mount(param1)
|
||||
tool_ctl.update_peri_list()
|
||||
tool_ctl.update_relay_list()
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
|
||||
@@ -7,6 +7,20 @@ local util = require("scada-common.util")
|
||||
|
||||
local databus = {}
|
||||
|
||||
local _dbus = {
|
||||
wd_modem = true,
|
||||
wl_modem = true,
|
||||
coroutines = {}
|
||||
}
|
||||
|
||||
-- evaluate and publish system health status
|
||||
local function eval_status()
|
||||
local ok = _dbus.wd_modem and _dbus.wl_modem
|
||||
for _, v in pairs(_dbus.coroutines) do ok = ok and v end
|
||||
|
||||
databus.ps.publish("status", ok)
|
||||
end
|
||||
|
||||
-- databus PSIL
|
||||
databus.ps = psil.create()
|
||||
|
||||
@@ -23,7 +37,7 @@ databus.RTU_HW_STATE = RTU_HW_STATE
|
||||
-- call to toggle heartbeat signal
|
||||
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||
|
||||
-- transmit firmware versions across the bus
|
||||
-- transmit firmware versions
|
||||
---@param rtu_v string RTU version
|
||||
---@param comms_v string comms version
|
||||
function databus.tx_versions(rtu_v, comms_v)
|
||||
@@ -31,10 +45,34 @@ function databus.tx_versions(rtu_v, comms_v)
|
||||
databus.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- transmit hardware status for modem connection state
|
||||
-- transmit hardware status for the wired comms modem
|
||||
---@param has_modem boolean
|
||||
function databus.tx_hw_modem(has_modem)
|
||||
databus.ps.publish("has_modem", has_modem)
|
||||
function databus.tx_hw_wd_modem(has_modem)
|
||||
databus.ps.publish("has_wd_modem", has_modem)
|
||||
|
||||
_dbus.wd_modem = has_modem
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit hardware status for the wireless comms modem
|
||||
---@param has_modem boolean
|
||||
function databus.tx_hw_wl_modem(has_modem)
|
||||
databus.ps.publish("has_wl_modem", has_modem)
|
||||
|
||||
_dbus.wl_modem = has_modem
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit if the wired network is up
|
||||
---@param up boolean
|
||||
function databus.tx_wd_net(up)
|
||||
databus.ps.publish("has_wd_net", up)
|
||||
end
|
||||
|
||||
-- transmit if the wireless network is up
|
||||
---@param up boolean
|
||||
function databus.tx_wl_net(up)
|
||||
databus.ps.publish("has_wl_net", up)
|
||||
end
|
||||
|
||||
-- transmit the number of speakers connected
|
||||
@@ -43,14 +81,14 @@ function databus.tx_hw_spkr_count(count)
|
||||
databus.ps.publish("speaker_count", count)
|
||||
end
|
||||
|
||||
-- transmit unit hardware type across the bus
|
||||
-- transmit unit hardware type
|
||||
---@param uid integer unit ID
|
||||
---@param type RTU_UNIT_TYPE
|
||||
function databus.tx_unit_hw_type(uid, type)
|
||||
databus.ps.publish("unit_type_" .. uid, type)
|
||||
end
|
||||
|
||||
-- transmit unit hardware status across the bus
|
||||
-- transmit unit hardware status
|
||||
---@param uid integer unit ID
|
||||
---@param status RTU_HW_STATE
|
||||
function databus.tx_unit_hw_status(uid, status)
|
||||
@@ -61,20 +99,18 @@ end
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function databus.tx_rt_status(thread, ok)
|
||||
databus.ps.publish(util.c("routine__", thread), ok)
|
||||
local name = util.c("routine__", thread)
|
||||
|
||||
databus.ps.publish(name, ok)
|
||||
|
||||
_dbus.coroutines[name] = ok
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit supervisor link state across the bus
|
||||
-- transmit supervisor link state
|
||||
---@param state integer
|
||||
function databus.tx_link_state(state)
|
||||
databus.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
-- link a function to receive data from the bus
|
||||
---@param field string field name
|
||||
---@param func function function to link
|
||||
function databus.rx_field(field, func)
|
||||
databus.ps.subscribe(field, func)
|
||||
end
|
||||
|
||||
return databus
|
||||
|
||||
116
rtu/modbus.lua
116
rtu/modbus.lua
@@ -300,22 +300,22 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
|
||||
-- validate a request without actually executing it
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.check_request(packet)
|
||||
---@param adu modbus_adu
|
||||
---@return boolean return_code, modbus_container reply
|
||||
function public.check_request(adu)
|
||||
local return_code = true
|
||||
local response = { MODBUS_EXCODE.ACKNOWLEDGE }
|
||||
|
||||
if packet.length == 2 then
|
||||
if adu.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
|
||||
if adu.func_code == MODBUS_FCODE.READ_COILS then
|
||||
elseif adu.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then
|
||||
elseif adu.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then
|
||||
elseif adu.func_code == MODBUS_FCODE.READ_INPUT_REGS then
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_MUL_COILS then
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
|
||||
else
|
||||
-- unknown function
|
||||
return_code = false
|
||||
@@ -329,41 +329,41 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
|
||||
-- default is to echo back<br>
|
||||
-- but here we echo back with error flag, on success the "error" will be acknowledgement
|
||||
local func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
local func_code = bit.bor(adu.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
|
||||
-- create reply
|
||||
local reply = comms.modbus_packet()
|
||||
reply.make(packet.txn_id, packet.unit_id, func_code, response)
|
||||
local reply = comms.modbus_container()
|
||||
reply.make(adu.txn_id, adu.unit_id, func_code, response)
|
||||
|
||||
return return_code, reply
|
||||
end
|
||||
|
||||
-- handle a MODBUS TCP packet and generate a reply
|
||||
-- handle a MODBUS TCP ADU and generate a reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.handle_packet(packet)
|
||||
---@param adu modbus_adu
|
||||
---@return boolean return_code, modbus_container reply
|
||||
function public.handle_adu(adu)
|
||||
local return_code ---@type boolean
|
||||
local response ---@type table|MODBUS_EXCODE
|
||||
|
||||
if packet.length >= 2 then
|
||||
if adu.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], { table.unpack(packet.data, 2, packet.length) })
|
||||
elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
|
||||
return_code, response = _16_write_multiple_holding_registers(packet.data[1], { table.unpack(packet.data, 2, packet.length) })
|
||||
if adu.func_code == MODBUS_FCODE.READ_COILS then
|
||||
return_code, response = _1_read_coils(adu.data[1], adu.data[2])
|
||||
elseif adu.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then
|
||||
return_code, response = _2_read_discrete_inputs(adu.data[1], adu.data[2])
|
||||
elseif adu.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then
|
||||
return_code, response = _3_read_multiple_holding_registers(adu.data[1], adu.data[2])
|
||||
elseif adu.func_code == MODBUS_FCODE.READ_INPUT_REGS then
|
||||
return_code, response = _4_read_input_registers(adu.data[1], adu.data[2])
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then
|
||||
return_code, response = _5_write_single_coil(adu.data[1], adu.data[2])
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then
|
||||
return_code, response = _6_write_single_holding_register(adu.data[1], adu.data[2])
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_MUL_COILS then
|
||||
return_code, response = _15_write_multiple_coils(adu.data[1], { table.unpack(adu.data, 2, adu.length) })
|
||||
elseif adu.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then
|
||||
return_code, response = _16_write_multiple_holding_registers(adu.data[1], { table.unpack(adu.data, 2, adu.length) })
|
||||
else
|
||||
-- unknown function
|
||||
return_code = false
|
||||
@@ -376,10 +376,10 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- default is to echo back
|
||||
local func_code = packet.func_code
|
||||
local func_code = adu.func_code
|
||||
if not return_code then
|
||||
-- echo back with error flag
|
||||
func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
func_code = bit.bor(adu.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
end
|
||||
|
||||
if type(response) == "table" then
|
||||
@@ -390,8 +390,8 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- create reply
|
||||
local reply = comms.modbus_packet()
|
||||
reply.make(packet.txn_id, packet.unit_id, func_code, response)
|
||||
local reply = comms.modbus_container()
|
||||
reply.make(adu.txn_id, adu.unit_id, func_code, response)
|
||||
|
||||
return return_code, reply
|
||||
end
|
||||
@@ -401,39 +401,39 @@ end
|
||||
|
||||
-- create an error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@param adu modbus_adu MODBUS ADU
|
||||
---@param code MODBUS_EXCODE exception code
|
||||
---@return modbus_packet reply
|
||||
local function excode_reply(packet, code)
|
||||
---@return modbus_container reply
|
||||
local function excode_reply(adu, code)
|
||||
-- reply back with error flag and exception code
|
||||
local reply = comms.modbus_packet()
|
||||
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
reply.make(packet.txn_id, packet.unit_id, fcode, { code })
|
||||
local reply = comms.modbus_container()
|
||||
local fcode = bit.bor(adu.func_code, MODBUS_FCODE.ERROR_FLAG)
|
||||
reply.make(adu.txn_id, adu.unit_id, fcode, { code })
|
||||
return reply
|
||||
end
|
||||
|
||||
-- return a SERVER_DEVICE_FAIL error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__srv_device_fail(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end
|
||||
---@param adu modbus_adu MODBUS ADU
|
||||
---@return modbus_container reply
|
||||
function modbus.reply__srv_device_fail(adu) return excode_reply(adu, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end
|
||||
|
||||
-- return a SERVER_DEVICE_BUSY error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__srv_device_busy(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end
|
||||
---@param adu modbus_adu MODBUS ADU
|
||||
---@return modbus_container reply
|
||||
function modbus.reply__srv_device_busy(adu) return excode_reply(adu, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end
|
||||
|
||||
-- return a NEG_ACKNOWLEDGE error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__neg_ack(packet) return excode_reply(packet, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end
|
||||
---@param adu modbus_adu MODBUS ADU
|
||||
---@return modbus_container reply
|
||||
function modbus.reply__neg_ack(adu) return excode_reply(adu, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end
|
||||
|
||||
-- return a GATEWAY_PATH_UNAVAILABLE error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__gw_unavailable(packet) return excode_reply(packet, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end
|
||||
---@param adu modbus_adu MODBUS ADU
|
||||
---@return modbus_container reply
|
||||
function modbus.reply__gw_unavailable(adu) return excode_reply(adu, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end
|
||||
|
||||
return modbus
|
||||
|
||||
@@ -12,6 +12,7 @@ local style = require("rtu.panel.style")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.Div")
|
||||
local Rectangle = require("graphics.elements.Rectangle")
|
||||
local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
|
||||
@@ -25,6 +26,7 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
|
||||
@@ -32,8 +34,11 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel DisplayBox main displaybox
|
||||
---@param config rtu_config configuraiton
|
||||
---@param units rtu_registry_entry[] unit list
|
||||
local function init(panel, units)
|
||||
local function init(panel, config, units)
|
||||
local s_hi_box = style.theme.highlight_box
|
||||
|
||||
local disabled_fg = style.fp.disabled_fg
|
||||
|
||||
local term_w, term_h = term.getSize()
|
||||
@@ -46,14 +51,53 @@ local function init(panel, units)
|
||||
|
||||
local system = Div{parent=panel,width=14,height=term_h-5,x=2,y=3}
|
||||
|
||||
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
|
||||
on.update(true)
|
||||
system.line_break()
|
||||
|
||||
status.register(databus.ps, "status", status.update)
|
||||
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
|
||||
if config.WirelessModem and config.WiredModem then
|
||||
local wd_modem = LEDPair{parent=system,label="WD MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
local wl_modem = LEDPair{parent=system,label="WL MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
local function wd_modem_update()
|
||||
if databus.ps.get("has_wd_modem") then
|
||||
if databus.ps.get("has_wd_net") then
|
||||
wd_modem.update(3)
|
||||
else wd_modem.update(2) end
|
||||
else wd_modem.update(1) end
|
||||
end
|
||||
|
||||
local function wl_modem_update()
|
||||
if databus.ps.get("has_wl_modem") then
|
||||
if databus.ps.get("has_wl_net") then
|
||||
wl_modem.update(3)
|
||||
else wl_modem.update(2) end
|
||||
else wl_modem.update(1) end
|
||||
end
|
||||
|
||||
wd_modem.register(databus.ps, "has_wd_modem", wd_modem_update)
|
||||
wd_modem.register(databus.ps, "has_wd_net", wd_modem_update)
|
||||
wl_modem.register(databus.ps, "has_wl_modem", wl_modem_update)
|
||||
wl_modem.register(databus.ps, "has_wl_net", wl_modem_update)
|
||||
else
|
||||
local modem = LEDPair{parent=system,label="MODEM",off=colors.green_off,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
local pfx = util.trinary(config.WirelessModem, "has_wl_", "has_wd_")
|
||||
|
||||
local function modem_update()
|
||||
if databus.ps.get(pfx .. "modem") then
|
||||
if databus.ps.get(pfx .. "net") then
|
||||
modem.update(3)
|
||||
else modem.update(2) end
|
||||
else modem.update(1) end
|
||||
end
|
||||
|
||||
modem.register(databus.ps, pfx .. "modem", modem_update)
|
||||
modem.register(databus.ps, pfx .. "net", modem_update)
|
||||
end
|
||||
|
||||
if not style.colorblind then
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
|
||||
@@ -90,8 +134,6 @@ local function init(panel, units)
|
||||
|
||||
system.line_break()
|
||||
|
||||
modem.register(databus.ps, "has_modem", modem.update)
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
|
||||
local rt_comm = LED{parent=system,label="RT COMMS",colors=ind_grn}
|
||||
system.line_break()
|
||||
@@ -99,25 +141,27 @@ local function init(panel, units)
|
||||
rt_main.register(databus.ps, "routine__main", rt_main.update)
|
||||
rt_comm.register(databus.ps, "routine__comms", rt_comm.update)
|
||||
|
||||
--
|
||||
-- hardware labeling
|
||||
--
|
||||
|
||||
local hw_labels = Rectangle{parent=panel,x=2,y=term_h-6,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
|
||||
local comp_id = util.sprintf("%03d", os.getComputerID())
|
||||
|
||||
TextBox{parent=system,y=term_h-5,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
|
||||
local speaker_count = DataIndicator{parent=system,x=10,y=term_h-5,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
|
||||
TextBox{parent=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
|
||||
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
|
||||
TextBox{parent=hw_labels,text="SN "..comp_id.."-RTU",fg_bg=s_hi_box}
|
||||
|
||||
--
|
||||
-- speaker count
|
||||
--
|
||||
|
||||
TextBox{parent=panel,x=2,y=term_h-1,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
|
||||
local speaker_count = DataIndicator{parent=panel,x=11,y=term_h-1,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
|
||||
speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
|
||||
|
||||
--
|
||||
-- about label
|
||||
--
|
||||
|
||||
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
|
||||
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
|
||||
|
||||
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
|
||||
--
|
||||
-- unit status list
|
||||
--
|
||||
@@ -129,7 +173,7 @@ local function init(panel, units)
|
||||
|
||||
-- show routine statuses
|
||||
for i = 1, list_length do
|
||||
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)}
|
||||
TextBox{parent=threads,y=i,text=util.sprintf("%02d",i)}
|
||||
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=util.trinary(units[i].type~=RTU_UNIT_TYPE.REDSTONE,ind_grn,cpair(style.ind_bkg,style.ind_bkg))}
|
||||
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
|
||||
end
|
||||
|
||||
@@ -18,16 +18,15 @@ local ui = {
|
||||
}
|
||||
|
||||
-- try to start the UI
|
||||
---@param config rtu_config configuration
|
||||
---@param units rtu_registry_entry[] RTU entries
|
||||
---@param theme FP_THEME front panel theme
|
||||
---@param color_mode COLOR_MODE color mode
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui(units, theme, color_mode)
|
||||
function renderer.try_start_ui(config, units)
|
||||
local status, msg = true, nil
|
||||
|
||||
if ui.display == nil then
|
||||
-- set theme
|
||||
style.set_theme(theme, color_mode)
|
||||
style.set_theme(config.FrontPanelTheme, config.ColorMode)
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
@@ -41,7 +40,7 @@ function renderer.try_start_ui(units, theme, color_mode)
|
||||
end
|
||||
|
||||
-- apply color mode
|
||||
local c_mode_overrides = style.theme.color_modes[color_mode]
|
||||
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
|
||||
for i = 1, #c_mode_overrides do
|
||||
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
|
||||
end
|
||||
@@ -49,7 +48,7 @@ function renderer.try_start_ui(units, theme, color_mode)
|
||||
-- init front panel view
|
||||
status, msg = pcall(function ()
|
||||
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
|
||||
panel_view(ui.display, units)
|
||||
panel_view(ui.display, config, units)
|
||||
end)
|
||||
|
||||
if status then
|
||||
|
||||
149
rtu/rtu.lua
149
rtu/rtu.lua
@@ -18,6 +18,10 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
-- wait 5 seconds after initializing a network switch request before being allowed to send more,
|
||||
-- which avoids repeat duplicate requests
|
||||
local FAILOVER_GRACE_PERIOD_MS = 5000
|
||||
|
||||
---@type rtu_config
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local config = {}
|
||||
@@ -33,6 +37,9 @@ function rtu.load_config()
|
||||
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
|
||||
config.WirelessModem = settings.get("WirelessModem")
|
||||
config.WiredModem = settings.get("WiredModem")
|
||||
config.PreferWireless = settings.get("PreferWireless")
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.RTU_Channel = settings.get("RTU_Channel")
|
||||
config.ConnTimeout = settings.get("ConnTimeout")
|
||||
@@ -57,6 +64,10 @@ function rtu.validate_config(cfg)
|
||||
cfv.assert_type_num(cfg.SpeakerVolume)
|
||||
cfv.assert_range(cfg.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_type_bool(cfg.WirelessModem)
|
||||
cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string"))
|
||||
cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string"))
|
||||
cfv.assert_type_bool(cfg.PreferWireless)
|
||||
cfv.assert_channel(cfg.SVR_Channel)
|
||||
cfv.assert_channel(cfg.RTU_Channel)
|
||||
cfv.assert_type_num(cfg.ConnTimeout)
|
||||
@@ -286,38 +297,38 @@ end
|
||||
-- RTU Communications
|
||||
---@nodiscard
|
||||
---@param version string RTU version
|
||||
---@param nic nic network interface device
|
||||
---@param backplane rtu_backplane RTU backplane
|
||||
---@param conn_watchdog watchdog watchdog reference
|
||||
function rtu.comms(version, nic, conn_watchdog)
|
||||
function rtu.comms(version, backplane, conn_watchdog)
|
||||
local self = {
|
||||
sv_addr = comms.BROADCAST,
|
||||
seq_num = util.time_ms() * 10, -- unique per peer, restarting will not re-use seq nums due to message rate
|
||||
r_seq_num = nil, ---@type nil|integer
|
||||
txn_id = 0,
|
||||
failover_init = 0,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
}
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
local tx_nic = backplane.active_nic()
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
if config.WirelessModem then
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
end
|
||||
|
||||
-- configure modem channels
|
||||
nic.closeAll()
|
||||
nic.open(config.RTU_Channel)
|
||||
--#region PRIVATE FUNCTIONS --
|
||||
|
||||
-- send a scada management packet
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
local frame, mgmt = comms.scada_frame(), comms.mgmt_container()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
mgmt.make(msg_type, msg)
|
||||
frame.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, mgmt.raw_packet())
|
||||
|
||||
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
|
||||
tx_nic.transmit(config.SVR_Channel, config.RTU_Channel, frame)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@@ -345,18 +356,44 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
return advertisement
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
--#endregion
|
||||
|
||||
--#region PUBLIC FUNCTIONS --
|
||||
|
||||
---@class rtu_comms
|
||||
local public = {}
|
||||
|
||||
-- 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.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
-- switch the current active NIC
|
||||
---@param new_nic nic
|
||||
---@param rtu_state rtu_state
|
||||
function public.switch_nic(new_nic, rtu_state)
|
||||
if tx_nic.is_connected() then
|
||||
-- try to gracefully switch, we have an intact continuous connection
|
||||
log.info(util.c("switching link to reconnected interface ", new_nic.phy_name(), " from ", tx_nic.phy_name()))
|
||||
|
||||
tx_nic = new_nic
|
||||
_send(MGMT_TYPE.SWITCH_NET, {})
|
||||
else
|
||||
-- can't gracefully switch, the other NIC was lost
|
||||
log.info(util.c("closing link on ", tx_nic.phy_name(), ", switching to ", new_nic.phy_name()))
|
||||
|
||||
tx_nic = new_nic
|
||||
conn_watchdog.cancel()
|
||||
public.unlink(rtu_state)
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the provided NIC is currently active, and if not, switch back to it
|
||||
---@param act_nic nic
|
||||
function public.manage_failover(act_nic)
|
||||
if (act_nic ~= tx_nic) and act_nic.is_network_up() and ((util.time_ms() - self.failover_init) > FAILOVER_GRACE_PERIOD_MS) then
|
||||
log.info(util.c("primary interface ", act_nic.phy_name(), " is up, requesting link switch"))
|
||||
|
||||
tx_nic = act_nic
|
||||
_send(MGMT_TYPE.SWITCH_NET, {})
|
||||
|
||||
self.failover_init = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink from the server
|
||||
@@ -372,15 +409,31 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
---@param rtu_state rtu_state
|
||||
function public.close(rtu_state)
|
||||
conn_watchdog.cancel()
|
||||
public.unlink(rtu_state)
|
||||
_send(MGMT_TYPE.CLOSE, {})
|
||||
public.unlink(rtu_state)
|
||||
end
|
||||
|
||||
-- send a MODBUS TCP packet
|
||||
---@param m_cnt modbus_container
|
||||
function public.send_modbus(m_cnt)
|
||||
local frame = comms.scada_frame()
|
||||
|
||||
frame.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_cnt.raw_packet())
|
||||
|
||||
tx_nic.transmit(config.SVR_Channel, config.RTU_Channel, frame)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send establish request (includes advertisement)
|
||||
---@param units table
|
||||
function public.send_establish(units)
|
||||
function public.send_establish(nic, units)
|
||||
local ini_nic = tx_nic
|
||||
tx_nic = nic
|
||||
|
||||
self.r_seq_num = nil
|
||||
_send(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) })
|
||||
|
||||
tx_nic = ini_nic
|
||||
end
|
||||
|
||||
-- send capability advertisement
|
||||
@@ -402,34 +455,31 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return modbus_frame|mgmt_frame|nil packet
|
||||
---@return modbus_adu|mgmt_packet|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local pkt, nic = nil, backplane.nics[side]
|
||||
|
||||
if s_pkt then
|
||||
-- get as MODBUS TCP packet
|
||||
if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
local m_pkt = comms.modbus_packet()
|
||||
if m_pkt.decode(s_pkt) then
|
||||
pkt = m_pkt.get()
|
||||
if nic then
|
||||
local frame = nic.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if frame then
|
||||
if frame.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
pkt = comms.modbus_container().decode(frame)
|
||||
elseif frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_container().decode(frame)
|
||||
else
|
||||
log.debug("illegal packet type " .. frame.protocol(), true)
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
else
|
||||
log.error("parse_packet(" .. side .. "): received a packet from an interface without a nic?")
|
||||
end
|
||||
|
||||
return pkt
|
||||
end
|
||||
|
||||
-- handle a MODBUS/SCADA packet
|
||||
---@param packet modbus_frame|mgmt_frame
|
||||
---@param packet modbus_adu|mgmt_packet
|
||||
---@param units rtu_registry_entry[] RTU entries
|
||||
---@param rtu_state rtu_state
|
||||
---@param sounders rtu_speaker_sounder[] speaker alarm sounders
|
||||
@@ -461,10 +511,10 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOL.MODBUS_TCP then
|
||||
---@cast packet modbus_frame
|
||||
---@cast packet modbus_adu
|
||||
if rtu_state.linked then
|
||||
local return_code ---@type boolean
|
||||
local reply ---@type modbus_packet
|
||||
local reply ---@type modbus_container
|
||||
|
||||
-- handle MODBUS instruction
|
||||
if packet.unit_id <= #units then
|
||||
@@ -473,7 +523,7 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
|
||||
if unit.type == RTU_UNIT_TYPE.REDSTONE then
|
||||
-- immediately execute redstone RTU requests
|
||||
return_code, reply = unit.modbus_io.handle_packet(packet)
|
||||
return_code, reply = unit.modbus_io.handle_adu(packet)
|
||||
|
||||
if not return_code then
|
||||
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
|
||||
@@ -488,7 +538,7 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
log.warning("device busy, discarding new request" .. unit_dbg_tag)
|
||||
else
|
||||
-- queue the command if not busy
|
||||
unit.pkt_queue.push_packet(packet)
|
||||
unit.pkt_queue.push_network(packet)
|
||||
end
|
||||
else
|
||||
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
|
||||
@@ -505,7 +555,7 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
log.debug("discarding MODBUS packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
---@cast packet mgmt_packet
|
||||
-- SCADA management packet
|
||||
if rtu_state.linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
@@ -553,10 +603,13 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
-- establish allowed
|
||||
tx_nic = backplane.nics[packet.scada_frame.interface()]
|
||||
|
||||
rtu_state.linked = true
|
||||
self.sv_addr = packet.scada_frame.src_addr()
|
||||
|
||||
println_ts("supervisor connection established")
|
||||
log.info("supervisor connection established")
|
||||
log.info(util.c("supervisor connection established, linked to SV (CID#", src_addr, ") on ", tx_nic.phy_name()))
|
||||
else
|
||||
-- establish denied
|
||||
if est_ack ~= self.last_est_ack then
|
||||
@@ -594,6 +647,8 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
521
rtu/startup.lua
521
rtu/startup.lua
@@ -1,40 +1,27 @@
|
||||
--
|
||||
-- RTU: Remote Terminal Unit
|
||||
-- RTU Gateway: Remote Terminal Unit Gateway
|
||||
--
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local audio = require("scada-common.audio")
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local network = require("scada-common.network")
|
||||
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 audio = require("scada-common.audio")
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local configure = require("rtu.configure")
|
||||
local databus = require("rtu.databus")
|
||||
local modbus = require("rtu.modbus")
|
||||
local renderer = require("rtu.renderer")
|
||||
local rtu = require("rtu.rtu")
|
||||
local threads = require("rtu.threads")
|
||||
local backplane = require("rtu.backplane")
|
||||
local configure = require("rtu.configure")
|
||||
local databus = require("rtu.databus")
|
||||
local renderer = require("rtu.renderer")
|
||||
local rtu = require("rtu.rtu")
|
||||
local threads = require("rtu.threads")
|
||||
local uinit = require("rtu.uinit")
|
||||
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local redstone_rtu = require("rtu.dev.redstone_rtu")
|
||||
local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "v1.12.3"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_HW_STATE = databus.RTU_HW_STATE
|
||||
local RTU_VERSION = "v1.13.11"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -82,7 +69,7 @@ local function main()
|
||||
-- startup
|
||||
----------------------------------------
|
||||
|
||||
-- record firmware versions and ID
|
||||
-- report versions
|
||||
databus.tx_versions(RTU_VERSION, comms.version)
|
||||
|
||||
-- mount connected devices
|
||||
@@ -106,15 +93,9 @@ local function main()
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- RTU gateway devices (not RTU units)
|
||||
rtu_dev = {
|
||||
modem = ppm.get_wireless_modem(),
|
||||
sounders = {} ---@type rtu_speaker_sounder[]
|
||||
},
|
||||
|
||||
-- system objects
|
||||
---@class rtu_sys
|
||||
rtu_sys = {
|
||||
nic = nil, ---@type nic
|
||||
rtu_comms = nil, ---@type rtu_comms
|
||||
conn_watchdog = nil, ---@type watchdog
|
||||
units = {} ---@type rtu_registry_entry[]
|
||||
@@ -126,468 +107,23 @@ local function main()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_sys = __shared_memory.rtu_sys
|
||||
local smem_dev = __shared_memory.rtu_dev
|
||||
|
||||
local smem_sys = __shared_memory.rtu_sys
|
||||
local rtu_state = __shared_memory.rtu_state
|
||||
local units = __shared_memory.rtu_sys.units
|
||||
|
||||
----------------------------------------
|
||||
-- interpret config and init units
|
||||
-- init and start system
|
||||
----------------------------------------
|
||||
|
||||
local units = __shared_memory.rtu_sys.units
|
||||
-- modem and speaker initialization
|
||||
if not backplane.init(config, __shared_memory) then return end
|
||||
|
||||
local rtu_redstone = config.Redstone
|
||||
local rtu_devices = config.Peripherals
|
||||
|
||||
-- get a string representation of a port interface
|
||||
---@param entry rtu_rs_definition
|
||||
---@return string
|
||||
local function entry_iface_name(entry)
|
||||
return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
|
||||
end
|
||||
|
||||
-- configure RTU gateway based on settings file definitions
|
||||
local function sys_config()
|
||||
--#region Redstone Interfaces
|
||||
|
||||
local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[]
|
||||
local all_conns = { [0] = {}, {}, {}, {}, {} }
|
||||
|
||||
-- go through redstone definitions list
|
||||
for entry_idx = 1, #rtu_redstone do
|
||||
local entry = rtu_redstone[entry_idx]
|
||||
|
||||
local assignment
|
||||
local for_reactor = entry.unit
|
||||
local phy = entry.relay or 0
|
||||
local phy_name = entry.relay or "local"
|
||||
local iface_name = entry_iface_name(entry)
|
||||
|
||||
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
|
||||
---@cast for_reactor integer
|
||||
assignment = "reactor unit " .. entry.unit
|
||||
elseif entry.unit == nil then
|
||||
assignment = "facility"
|
||||
for_reactor = 0
|
||||
else
|
||||
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- create the appropriate RTU if it doesn't exist and check relay name validity
|
||||
if entry.relay then
|
||||
if type(entry.relay) ~= "string" then
|
||||
local message = util.c("sys_config> invalid redstone relay '", entry.relay, '"')
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
elseif not rs_rtus[entry.relay] then
|
||||
log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay))
|
||||
|
||||
local hw_state = RTU_HW_STATE.OK
|
||||
local relay = ppm.get_periph(entry.relay)
|
||||
|
||||
if not relay then
|
||||
hw_state = RTU_HW_STATE.OFFLINE
|
||||
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected"))
|
||||
local _, v_device = ppm.mount_virtual()
|
||||
relay = v_device
|
||||
elseif ppm.get_type(entry.relay) ~= "redstone_relay" then
|
||||
hw_state = RTU_HW_STATE.FAULTED
|
||||
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay"))
|
||||
end
|
||||
|
||||
rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } }
|
||||
end
|
||||
elseif rs_rtus[0] == nil then
|
||||
log.debug(util.c("sys_config> allocated local redstone RTU"))
|
||||
rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } }
|
||||
end
|
||||
|
||||
-- verify configuration
|
||||
local valid = false
|
||||
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
|
||||
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
|
||||
end
|
||||
|
||||
local bank = rs_rtus[phy].banks[for_reactor]
|
||||
local conns = all_conns[for_reactor]
|
||||
|
||||
if not valid then
|
||||
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else
|
||||
-- link redstone in RTU
|
||||
local mode = rsio.get_io_mode(entry.port)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(conns, entry.port) then
|
||||
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
table.insert(bank, entry)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(conns, entry.port) then
|
||||
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
table.insert(bank, entry)
|
||||
end
|
||||
elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then
|
||||
table.insert(bank, entry)
|
||||
else
|
||||
-- should be unreachable code, we already validated ports
|
||||
log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx)
|
||||
println("sys_config> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
table.insert(conns, entry.port)
|
||||
|
||||
log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
|
||||
end
|
||||
end
|
||||
|
||||
-- create unit entries for redstone RTUs
|
||||
for _, def in pairs(rs_rtus) do
|
||||
local rtu_conns = { [0] = {}, {}, {}, {}, {} }
|
||||
|
||||
-- connect the IO banks
|
||||
for for_reactor = 0, #def.banks do
|
||||
local bank = def.banks[for_reactor]
|
||||
local conns = rtu_conns[for_reactor]
|
||||
local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility")
|
||||
|
||||
-- link redstone to the RTU
|
||||
for i = 1, #bank do
|
||||
local conn = bank[i]
|
||||
local phy_name = conn.relay or "local"
|
||||
|
||||
local mode = rsio.get_io_mode(conn.port)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
def.rtu.link_di(conn.side, conn.color, conn.invert)
|
||||
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
||||
def.rtu.link_do(conn.side, conn.color, conn.invert)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
def.rtu.link_ai(conn.side)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
||||
def.rtu.link_ao(conn.side)
|
||||
else
|
||||
log.fatal(util.c("sys_config> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
|
||||
println("sys_config> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
table.insert(conns, conn.port)
|
||||
|
||||
log.debug(util.c("sys_config> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign))
|
||||
end
|
||||
end
|
||||
|
||||
---@type rtu_registry_entry
|
||||
local unit = {
|
||||
uid = 0,
|
||||
name = def.name,
|
||||
type = RTU_UNIT_TYPE.REDSTONE,
|
||||
index = false,
|
||||
reactor = nil,
|
||||
device = def.phy,
|
||||
rs_conns = rtu_conns,
|
||||
is_multiblock = false,
|
||||
formed = nil,
|
||||
hw_state = def.hw_state,
|
||||
rtu = def.rtu,
|
||||
modbus_io = modbus.new(def.rtu, false),
|
||||
pkt_queue = nil,
|
||||
thread = nil
|
||||
}
|
||||
|
||||
table.insert(units, unit)
|
||||
|
||||
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
|
||||
|
||||
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
|
||||
|
||||
unit.uid = #units
|
||||
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
--#region Mounted Peripherals
|
||||
|
||||
for i = 1, #rtu_devices do
|
||||
local entry = rtu_devices[i] ---@type rtu_peri_definition
|
||||
local name = entry.name
|
||||
local index = entry.index
|
||||
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
|
||||
|
||||
-- CHECK: name is a string
|
||||
if type(name) ~= "string" then
|
||||
local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index type
|
||||
if (index ~= nil) and (not util.is_int(index)) then
|
||||
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index range
|
||||
local function validate_index(min, max)
|
||||
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
|
||||
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
|
||||
if max ~= nil then message = util.c(message, " and <= ", max) end
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- CHECK: reactor is an integer >= 0
|
||||
local function validate_assign(for_facility)
|
||||
if for_facility and for_reactor ~= 0 then
|
||||
local message = util.c("sys_config> device entry #", i, ": must only be for the facility")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
|
||||
local message = util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
local device = ppm.get_periph(name)
|
||||
|
||||
local type ---@type string|nil
|
||||
local rtu_iface ---@type rtu_device
|
||||
local rtu_type ---@type RTU_UNIT_TYPE
|
||||
local is_multiblock = false ---@type boolean
|
||||
local formed = nil ---@type boolean|nil
|
||||
local faulted = nil ---@type boolean|nil
|
||||
|
||||
if device == nil then
|
||||
local message = util.c("sys_config> '", name, "' not found, using placeholder")
|
||||
println(message)
|
||||
log.warning(message)
|
||||
|
||||
-- mount a virtual (placeholder) device
|
||||
type, device = ppm.mount_virtual()
|
||||
else
|
||||
type = ppm.get_type(name)
|
||||
end
|
||||
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
if not validate_index(1, 2) then return false end
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
rtu_iface, faulted = boilerv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
|
||||
end
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
if not validate_index(1, 3) then return false end
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
rtu_iface, faulted = turbinev_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
|
||||
end
|
||||
elseif type == "dynamicValve" then
|
||||
-- dynamic tank multiblock
|
||||
if entry.unit == nil then
|
||||
if not validate_index(1, 4) then return false end
|
||||
if not validate_assign(true) then return false end
|
||||
else
|
||||
if not validate_index(1, 1) then return false end
|
||||
if not validate_assign() then return false end
|
||||
end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||
rtu_iface, faulted = dynamicv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
||||
end
|
||||
elseif type == "inductionPort" or type == "reinforcedInductionPort" then
|
||||
-- induction matrix multiblock (normal or reinforced)
|
||||
if not validate_assign(true) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
||||
rtu_iface, faulted = imatrix_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
|
||||
end
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
if not validate_assign(true) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.SPS
|
||||
rtu_iface, faulted = sps_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
|
||||
end
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.SNA
|
||||
rtu_iface, faulted = sna_rtu.new(device)
|
||||
elseif type == "environmentDetector" or type == "environment_detector" then
|
||||
-- advanced peripherals environment detector
|
||||
if not validate_index(1) then return false end
|
||||
if not validate_assign(entry.unit == nil) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
rtu_iface, faulted = envd_rtu.new(device)
|
||||
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
||||
-- placeholder device
|
||||
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
||||
rtu_iface = rtu.init_unit().interface()
|
||||
else
|
||||
local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
|
||||
println_ts(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
if is_multiblock then
|
||||
if not formed then
|
||||
if formed == false then
|
||||
log.info(util.c("sys_config> device '", name, "' is not formed"))
|
||||
else formed = false end
|
||||
elseif faulted then
|
||||
-- sometimes there is a race condition on server boot where it reports formed, but
|
||||
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
|
||||
formed = false
|
||||
log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
|
||||
end
|
||||
end
|
||||
|
||||
---@class rtu_registry_entry
|
||||
local rtu_unit = {
|
||||
uid = 0, ---@type integer RTU unit ID
|
||||
name = name, ---@type string unit name
|
||||
type = rtu_type, ---@type RTU_UNIT_TYPE unit type
|
||||
index = index or false, ---@type integer|false device index
|
||||
reactor = for_reactor, ---@type integer|nil unit/facility assignment
|
||||
device = device, ---@type table peripheral reference
|
||||
rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections
|
||||
is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral
|
||||
formed = formed, ---@type boolean|nil if this peripheral is currently formed
|
||||
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status
|
||||
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface
|
||||
modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface
|
||||
pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue
|
||||
thread = nil ---@type parallel_thread|nil associated RTU thread
|
||||
}
|
||||
|
||||
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
|
||||
|
||||
table.insert(units, rtu_unit)
|
||||
|
||||
local for_message = "the facility"
|
||||
if for_reactor > 0 then
|
||||
for_message = util.c("reactor ", for_reactor)
|
||||
end
|
||||
|
||||
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
|
||||
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
|
||||
|
||||
rtu_unit.uid = #units
|
||||
|
||||
-- determine hardware status
|
||||
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
rtu_unit.hw_state = RTU_HW_STATE.OFFLINE
|
||||
else
|
||||
if rtu_unit.is_multiblock then
|
||||
rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED)
|
||||
elseif faulted then
|
||||
rtu_unit.hw_state = RTU_HW_STATE.FAULTED
|
||||
else
|
||||
rtu_unit.hw_state = RTU_HW_STATE.OK
|
||||
end
|
||||
end
|
||||
|
||||
-- report hardware status
|
||||
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
log.debug("boot> running sys_config()")
|
||||
|
||||
if sys_config() then
|
||||
-- check modem
|
||||
if smem_dev.modem == nil then
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
return
|
||||
end
|
||||
|
||||
databus.tx_hw_modem(true)
|
||||
|
||||
-- find and setup all speakers
|
||||
local speakers = ppm.get_all_devices("speaker")
|
||||
for _, s in pairs(speakers) do
|
||||
local sounder = rtu.init_sounder(s)
|
||||
|
||||
table.insert(smem_dev.sounders, sounder)
|
||||
|
||||
log.debug(util.c("startup> added speaker, attached as ", sounder.name))
|
||||
end
|
||||
|
||||
databus.tx_hw_spkr_count(#smem_dev.sounders)
|
||||
log.debug("startup> running uinit()")
|
||||
|
||||
if uinit(config, __shared_memory) then
|
||||
-- start UI
|
||||
local message
|
||||
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
|
||||
rtu_state.fp_ok, message = renderer.try_start_ui(config, units)
|
||||
|
||||
if not rtu_state.fp_ok then
|
||||
println_ts(util.c("UI error: ", message))
|
||||
@@ -601,8 +137,7 @@ local function main()
|
||||
log.debug("startup> conn watchdog started")
|
||||
|
||||
-- setup comms
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, backplane, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
|
||||
-- init threads
|
||||
|
||||
237
rtu/threads.lua
237
rtu/threads.lua
@@ -5,10 +5,10 @@ local tcd = require("scada-common.tcd")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local backplane = require("rtu.backplane")
|
||||
local databus = require("rtu.databus")
|
||||
local modbus = require("rtu.modbus")
|
||||
local renderer = require("rtu.renderer")
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
|
||||
@@ -25,8 +25,8 @@ local threads = {}
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_HW_STATE = databus.RTU_HW_STATE
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
|
||||
local COMMS_SLEEP = 100 -- 100ms, 2 ticks
|
||||
|
||||
---@param smem rtu_shared_memory
|
||||
---@param println_ts function
|
||||
@@ -184,19 +184,54 @@ function threads.thread__main(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("main", true)
|
||||
log.debug("main thread start")
|
||||
log.debug("OS: 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 sounders = smem.rtu_dev.sounders
|
||||
local nic = smem.rtu_sys.nic
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local conn_watchdog = smem.rtu_sys.conn_watchdog
|
||||
local units = smem.rtu_sys.units
|
||||
|
||||
local sounders = backplane.sounders()
|
||||
|
||||
-- main loop periodic tasks
|
||||
local function loop_tick()
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- periodic hardware tasks
|
||||
backplane.periodic()
|
||||
|
||||
-- update speaker states
|
||||
for _, sounder in pairs(sounders) do
|
||||
-- re-compute output if needed, then play audio if available
|
||||
if sounder.stream.is_recompute_needed() then
|
||||
sounder.stream.compute_buffer()
|
||||
if sounder.stream.any_active() then sounder.play() else sounder.stop() end
|
||||
end
|
||||
end
|
||||
|
||||
-- period tick, if we are not linked send establish request
|
||||
if rtu_state.linked then
|
||||
rtu_comms.manage_failover(backplane.active_nic())
|
||||
else
|
||||
-- advertise units
|
||||
local a_nic, s_nic = backplane.active_nic(), backplane.standby_nic()
|
||||
|
||||
if a_nic.is_network_up() then
|
||||
rtu_comms.send_establish(a_nic, units)
|
||||
elseif s_nic and s_nic.is_network_up() then
|
||||
rtu_comms.send_establish(s_nic, units)
|
||||
end
|
||||
end
|
||||
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
end
|
||||
|
||||
-- start unlinked (in case of restart)
|
||||
rtu_comms.unlink(rtu_state)
|
||||
|
||||
@@ -207,77 +242,59 @@ function threads.thread__main(smem)
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
if event == "timer" and loop_clock.is_clock(param1) then
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- update speaker states
|
||||
for _, sounder in pairs(sounders) do
|
||||
-- re-compute output if needed, then play audio if available
|
||||
if sounder.stream.is_recompute_needed() then
|
||||
sounder.stream.compute_buffer()
|
||||
if sounder.stream.any_active() then sounder.play() else sounder.stop() end
|
||||
end
|
||||
end
|
||||
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- period tick, if we are not linked send establish request
|
||||
if not rtu_state.linked then
|
||||
-- advertise units
|
||||
rtu_comms.send_establish(units)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
if 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)
|
||||
smem.q.mq_comms.push_network(packet)
|
||||
end
|
||||
elseif event == "timer" and conn_watchdog.is_timer(param1) then
|
||||
-- haven't heard from server recently? close connection
|
||||
rtu_comms.close(rtu_state)
|
||||
elseif event == "timer" then
|
||||
-- notify timer callback dispatcher if no other timer case claimed this event
|
||||
tcd.handle(param1)
|
||||
-- pass this timer event onto the right handler
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
loop_tick()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor connection timed out
|
||||
rtu_comms.close(rtu_state)
|
||||
else
|
||||
-- notify timer callback dispatcher, no other handler claimed this event
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle empty speaker audio buffer
|
||||
for i = 1, #sounders do
|
||||
local sounder = sounders[i]
|
||||
if sounder.name == param1 then
|
||||
sounder.continue()
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "peripheral" then
|
||||
-- peripheral connect
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" or type == "speaker" then
|
||||
backplane.attach(type, device, param1, println_ts)
|
||||
else
|
||||
-- relink lost peripheral to correct unit entry
|
||||
for i = 1, #units do
|
||||
handle_unit_mount(smem, println_ts, param1, type, device, units[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
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
|
||||
---@cast device Modem
|
||||
-- we only care if this is our wireless modem
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.warning("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log.info("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
databus.tx_hw_modem(false)
|
||||
end
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
for i = 1, #sounders do
|
||||
if sounders[i].speaker == device then
|
||||
table.remove(sounders, i)
|
||||
|
||||
log.warning(util.c("speaker ", param1, " disconnected"))
|
||||
println_ts("speaker disconnected")
|
||||
|
||||
databus.tx_hw_spkr_count(#sounders)
|
||||
break
|
||||
end
|
||||
end
|
||||
if type == "modem" or type == "speaker" then
|
||||
backplane.detach(type, device, param1, println_ts)
|
||||
else
|
||||
for i = 1, #units do
|
||||
-- find disconnected device
|
||||
@@ -296,60 +313,12 @@ function threads.thread__main(smem)
|
||||
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
|
||||
---@cast device Modem
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
nic.connect(device)
|
||||
|
||||
println_ts("wireless modem reconnected.")
|
||||
log.info("comms modem reconnected")
|
||||
|
||||
databus.tx_hw_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log.info("wired modem reconnected")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
---@cast device Speaker
|
||||
table.insert(sounders, rtu.init_sounder(device))
|
||||
|
||||
println_ts("speaker connected")
|
||||
log.info(util.c("connected speaker ", param1))
|
||||
|
||||
databus.tx_hw_spkr_count(#sounders)
|
||||
else
|
||||
-- relink lost peripheral to correct unit entry
|
||||
for i = 1, #units do
|
||||
handle_unit_mount(smem, println_ts, param1, type, device, units[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle empty speaker audio buffer
|
||||
for i = 1, #sounders do
|
||||
local sounder = sounders[i]
|
||||
if sounder.name == param1 then
|
||||
sounder.continue()
|
||||
break
|
||||
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")
|
||||
log.info("OS: terminate requested, main thread exiting")
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -368,7 +337,7 @@ function threads.thread__main(smem)
|
||||
databus.tx_rt_status("main", false)
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info("main thread restarting in 5 seconds...")
|
||||
log.info("OS: main thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@@ -387,16 +356,16 @@ function threads.thread__comms(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms", true)
|
||||
log.debug("comms thread start")
|
||||
log.debug("OS: comms thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
local sounders = smem.rtu_dev.sounders
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local units = smem.rtu_sys.units
|
||||
|
||||
local comms_queue = smem.q.mq_comms
|
||||
|
||||
local sounders = backplane.sounders()
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
@@ -408,11 +377,7 @@ function threads.thread__comms(smem)
|
||||
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
|
||||
if msg.qtype == mqueue.TYPE.NETWORK then
|
||||
-- received a packet
|
||||
-- handle the packet (rtu_state passed to allow setting link flag, sounders passed to manage alarm audio)
|
||||
rtu_comms.handle_packet(msg.message, units, rtu_state, sounders)
|
||||
@@ -421,7 +386,7 @@ function threads.thread__comms(smem)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning("comms thread exceeded 100ms queue process limit")
|
||||
log.warning("OS: comms thread exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -432,7 +397,7 @@ function threads.thread__comms(smem)
|
||||
-- check for termination request
|
||||
if rtu_state.shutdown then
|
||||
rtu_comms.close(rtu_state)
|
||||
log.info("comms thread exiting")
|
||||
log.info("OS: comms thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@@ -454,7 +419,7 @@ function threads.thread__comms(smem)
|
||||
databus.tx_rt_status("comms", false)
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info("comms thread restarting in 5 seconds...")
|
||||
log.info("OS: comms thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@@ -477,7 +442,7 @@ function threads.thread__unit_comms(smem, unit)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("unit_" .. unit.uid, true)
|
||||
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
|
||||
log.debug(util.c("OS: rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
@@ -494,7 +459,7 @@ function threads.thread__unit_comms(smem, unit)
|
||||
local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
|
||||
|
||||
if packet_queue == nil then
|
||||
log.error("rtu unit thread created without a message queue, exiting...", true)
|
||||
log.error("OS: rtu unit thread created without a message queue, exiting...", true)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -505,13 +470,9 @@ function threads.thread__unit_comms(smem, unit)
|
||||
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)
|
||||
if msg.qtype == mqueue.TYPE.NETWORK then
|
||||
-- received an ADU
|
||||
local _, reply = unit.modbus_io.handle_adu(msg.message)
|
||||
rtu_comms.send_modbus(reply)
|
||||
end
|
||||
end
|
||||
@@ -522,7 +483,7 @@ function threads.thread__unit_comms(smem, unit)
|
||||
|
||||
-- check for termination request
|
||||
if rtu_state.shutdown then
|
||||
log.info("rtu unit thread exiting -> " .. short_name)
|
||||
log.info("OS: rtu unit thread exiting -> " .. short_name)
|
||||
break
|
||||
end
|
||||
|
||||
@@ -587,7 +548,7 @@ function threads.thread__unit_comms(smem, unit)
|
||||
databus.tx_rt_status("unit_" .. unit.uid, false)
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
|
||||
log.info(util.c("OS: rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
|
||||
441
rtu/uinit.lua
Normal file
441
rtu/uinit.lua
Normal file
@@ -0,0 +1,441 @@
|
||||
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 databus = require("rtu.databus")
|
||||
local modbus = require("rtu.modbus")
|
||||
local rtu = require("rtu.rtu")
|
||||
local threads = require("rtu.threads")
|
||||
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local redstone_rtu = require("rtu.dev.redstone_rtu")
|
||||
local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_HW_STATE = databus.RTU_HW_STATE
|
||||
|
||||
-- print and log a fatal error during startup
|
||||
---@param msg string
|
||||
local function log_fail(msg)
|
||||
println(msg)
|
||||
log.fatal(msg)
|
||||
end
|
||||
|
||||
-- get a string representation of a port interface
|
||||
---@param entry rtu_rs_definition
|
||||
---@return string
|
||||
local function entry_iface_name(entry)
|
||||
return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
|
||||
end
|
||||
|
||||
-- configure RTU gateway based on settings file definitions
|
||||
---@param config rtu_config
|
||||
---@param __shared_memory rtu_shared_memory
|
||||
---@return boolean success
|
||||
return function(config, __shared_memory)
|
||||
local units = __shared_memory.rtu_sys.units
|
||||
|
||||
local rtu_redstone = config.Redstone
|
||||
local rtu_devices = config.Peripherals
|
||||
|
||||
--#region Redstone Interfaces
|
||||
|
||||
local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[]
|
||||
local all_conns = { [0] = {}, {}, {}, {}, {} }
|
||||
|
||||
-- go through redstone definitions list
|
||||
for entry_idx = 1, #rtu_redstone do
|
||||
local entry = rtu_redstone[entry_idx]
|
||||
|
||||
local assignment
|
||||
local for_reactor = entry.unit
|
||||
local phy = entry.relay or 0
|
||||
local phy_name = entry.relay or "local"
|
||||
local iface_name = entry_iface_name(entry)
|
||||
|
||||
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
|
||||
---@cast for_reactor integer
|
||||
assignment = "reactor unit " .. entry.unit
|
||||
elseif entry.unit == nil then
|
||||
assignment = "facility"
|
||||
for_reactor = 0
|
||||
else
|
||||
log_fail(util.c("uinit> invalid unit assignment at block index #", entry_idx))
|
||||
return false
|
||||
end
|
||||
|
||||
-- create the appropriate RTU if it doesn't exist and check relay name validity
|
||||
if entry.relay then
|
||||
if type(entry.relay) ~= "string" then
|
||||
log_fail(util.c("uinit> invalid redstone relay '", entry.relay, '"'))
|
||||
return false
|
||||
elseif not rs_rtus[entry.relay] then
|
||||
log.debug(util.c("uinit> allocated relay redstone RTU on interface ", entry.relay))
|
||||
|
||||
local hw_state = RTU_HW_STATE.OK
|
||||
local relay = ppm.get_periph(entry.relay)
|
||||
|
||||
if not relay then
|
||||
hw_state = RTU_HW_STATE.OFFLINE
|
||||
log.warning(util.c("uinit> redstone relay ", entry.relay, " is not connected"))
|
||||
local _, v_device = ppm.mount_virtual()
|
||||
relay = v_device
|
||||
elseif ppm.get_type(entry.relay) ~= "redstone_relay" then
|
||||
hw_state = RTU_HW_STATE.FAULTED
|
||||
log.warning(util.c("uinit> redstone relay ", entry.relay, " is not a redstone relay"))
|
||||
end
|
||||
|
||||
rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } }
|
||||
end
|
||||
elseif rs_rtus[0] == nil then
|
||||
log.debug(util.c("uinit> allocated local redstone RTU"))
|
||||
rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } }
|
||||
end
|
||||
|
||||
-- verify configuration
|
||||
local valid = false
|
||||
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
|
||||
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
|
||||
end
|
||||
|
||||
local bank = rs_rtus[phy].banks[for_reactor]
|
||||
local conns = all_conns[for_reactor]
|
||||
|
||||
if not valid then
|
||||
log_fail(util.c("uinit> invalid redstone definition at block index #", entry_idx))
|
||||
return false
|
||||
else
|
||||
-- link redstone in RTU
|
||||
local mode = rsio.get_io_mode(entry.port)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(conns, entry.port) then
|
||||
local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
table.insert(bank, entry)
|
||||
end
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
-- can't have duplicate inputs
|
||||
if util.table_contains(conns, entry.port) then
|
||||
local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
|
||||
println(message)
|
||||
log.warning(message)
|
||||
else
|
||||
table.insert(bank, entry)
|
||||
end
|
||||
elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then
|
||||
table.insert(bank, entry)
|
||||
else
|
||||
-- should be unreachable code, we already validated ports
|
||||
log.fatal("uinit> failed to identify IO mode at block index #" .. entry_idx)
|
||||
println("uinit> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
table.insert(conns, entry.port)
|
||||
|
||||
log.debug(util.c("uinit> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
|
||||
end
|
||||
end
|
||||
|
||||
-- create unit entries for redstone RTUs
|
||||
for _, def in pairs(rs_rtus) do
|
||||
local rtu_conns = { [0] = {}, {}, {}, {}, {} }
|
||||
|
||||
-- connect the IO banks
|
||||
for for_reactor = 0, #def.banks do
|
||||
local bank = def.banks[for_reactor]
|
||||
local conns = rtu_conns[for_reactor]
|
||||
local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility")
|
||||
|
||||
-- link redstone to the RTU
|
||||
for i = 1, #bank do
|
||||
local conn = bank[i]
|
||||
local phy_name = conn.relay or "local"
|
||||
|
||||
local mode = rsio.get_io_mode(conn.port)
|
||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||
def.rtu.link_di(conn.side, conn.color, conn.invert)
|
||||
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
||||
def.rtu.link_do(conn.side, conn.color, conn.invert)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||
def.rtu.link_ai(conn.side)
|
||||
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
||||
def.rtu.link_ao(conn.side)
|
||||
else
|
||||
log.fatal(util.c("uinit> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
|
||||
println("uinit> encountered a software error, check logs")
|
||||
return false
|
||||
end
|
||||
|
||||
table.insert(conns, conn.port)
|
||||
|
||||
log.debug(util.c("uinit> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign))
|
||||
end
|
||||
end
|
||||
|
||||
---@type rtu_registry_entry
|
||||
local unit = {
|
||||
uid = 0,
|
||||
name = def.name,
|
||||
type = RTU_UNIT_TYPE.REDSTONE,
|
||||
index = false,
|
||||
reactor = nil,
|
||||
device = def.phy,
|
||||
rs_conns = rtu_conns,
|
||||
is_multiblock = false,
|
||||
formed = nil,
|
||||
hw_state = def.hw_state,
|
||||
rtu = def.rtu,
|
||||
modbus_io = modbus.new(def.rtu, false),
|
||||
pkt_queue = nil,
|
||||
thread = nil
|
||||
}
|
||||
|
||||
table.insert(units, unit)
|
||||
|
||||
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
|
||||
|
||||
log.info(util.c("uinit> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
|
||||
|
||||
unit.uid = #units
|
||||
|
||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
--#region Mounted Peripherals
|
||||
|
||||
for i = 1, #rtu_devices do
|
||||
local entry = rtu_devices[i] ---@type rtu_peri_definition
|
||||
local name = entry.name
|
||||
local index = entry.index
|
||||
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
|
||||
|
||||
-- CHECK: name is a string
|
||||
if type(name) ~= "string" then
|
||||
log_fail(util.c("uinit> device entry #", i, ": device ", name, " isn't a string"))
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index type
|
||||
if (index ~= nil) and (not util.is_int(index)) then
|
||||
log_fail(util.c("uinit> device entry #", i, ": index ", index, " isn't valid"))
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index range
|
||||
local function validate_index(min, max)
|
||||
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
|
||||
local message = util.c("uinit> device entry #", i, ": index ", index, " isn't >= ", min)
|
||||
if max ~= nil then message = util.c(message, " and <= ", max) end
|
||||
log_fail(message)
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- CHECK: reactor is an integer >= 0
|
||||
local function validate_assign(for_facility)
|
||||
if for_facility and for_reactor ~= 0 then
|
||||
log_fail(util.c("uinit> device entry #", i, ": must only be for the facility"))
|
||||
return false
|
||||
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
|
||||
log_fail(util.c("uinit> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild"))
|
||||
return false
|
||||
else return true end
|
||||
end
|
||||
|
||||
local device = ppm.get_periph(name)
|
||||
|
||||
local type ---@type string|nil
|
||||
local rtu_iface ---@type rtu_device
|
||||
local rtu_type ---@type RTU_UNIT_TYPE
|
||||
local is_multiblock = false ---@type boolean
|
||||
local formed = nil ---@type boolean|nil
|
||||
local faulted = nil ---@type boolean|nil
|
||||
|
||||
if device == nil then
|
||||
local message = util.c("uinit> '", name, "' not found, using placeholder")
|
||||
println(message)
|
||||
log.warning(message)
|
||||
|
||||
-- mount a virtual (placeholder) device
|
||||
type, device = ppm.mount_virtual()
|
||||
else
|
||||
type = ppm.get_type(name)
|
||||
end
|
||||
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
if not validate_index(1, 2) then return false end
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
rtu_iface, faulted = boilerv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("uinit> failed to check if '", name, "' is a formed boiler multiblock"))
|
||||
end
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
if not validate_index(1, 3) then return false end
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
rtu_iface, faulted = turbinev_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("uinit> failed to check if '", name, "' is a formed turbine multiblock"))
|
||||
end
|
||||
elseif type == "dynamicValve" then
|
||||
-- dynamic tank multiblock
|
||||
if entry.unit == nil then
|
||||
if not validate_index(1, 4) then return false end
|
||||
if not validate_assign(true) then return false end
|
||||
else
|
||||
if not validate_index(1, 1) then return false end
|
||||
if not validate_assign() then return false end
|
||||
end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||
rtu_iface, faulted = dynamicv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("uinit> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
||||
end
|
||||
elseif type == "inductionPort" or type == "reinforcedInductionPort" then
|
||||
-- induction matrix multiblock (normal or reinforced)
|
||||
if not validate_assign(true) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
||||
rtu_iface, faulted = imatrix_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("uinit> failed to check if '", name, "' is a formed induction matrix multiblock"))
|
||||
end
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
if not validate_assign(true) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.SPS
|
||||
rtu_iface, faulted = sps_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
|
||||
log.warning(util.c("uinit> failed to check if '", name, "' is a formed SPS multiblock"))
|
||||
end
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
if not validate_assign() then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.SNA
|
||||
rtu_iface, faulted = sna_rtu.new(device)
|
||||
elseif type == "environmentDetector" or type == "environment_detector" then
|
||||
-- advanced peripherals environment detector
|
||||
if not validate_index(1) then return false end
|
||||
if not validate_assign(entry.unit == nil) then return false end
|
||||
|
||||
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
rtu_iface, faulted = envd_rtu.new(device)
|
||||
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
||||
-- placeholder device
|
||||
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
||||
rtu_iface = rtu.init_unit().interface()
|
||||
else
|
||||
log_fail(util.c("uinit> device '", name, "' is not a known type (", type, ")"))
|
||||
return false
|
||||
end
|
||||
|
||||
if is_multiblock then
|
||||
if not formed then
|
||||
if formed == false then
|
||||
log.info(util.c("uinit> device '", name, "' is not formed"))
|
||||
else formed = false end
|
||||
elseif faulted then
|
||||
-- sometimes there is a race condition on server boot where it reports formed, but
|
||||
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
|
||||
formed = false
|
||||
log.warning(util.c("uinit> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
|
||||
end
|
||||
end
|
||||
|
||||
---@class rtu_registry_entry
|
||||
local rtu_unit = {
|
||||
uid = 0, ---@type integer RTU unit ID
|
||||
name = name, ---@type string unit name
|
||||
type = rtu_type, ---@type RTU_UNIT_TYPE unit type
|
||||
index = index or false, ---@type integer|false device index
|
||||
reactor = for_reactor, ---@type integer|nil unit/facility assignment
|
||||
device = device, ---@type table peripheral reference
|
||||
rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections
|
||||
is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral
|
||||
formed = formed, ---@type boolean|nil if this peripheral is currently formed
|
||||
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status
|
||||
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface
|
||||
modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface
|
||||
pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue
|
||||
thread = nil ---@type parallel_thread|nil associated RTU thread
|
||||
}
|
||||
|
||||
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
|
||||
|
||||
table.insert(units, rtu_unit)
|
||||
|
||||
local for_message = "the facility"
|
||||
if for_reactor > 0 then
|
||||
for_message = util.c("reactor ", for_reactor)
|
||||
end
|
||||
|
||||
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
|
||||
log.info(util.c("uinit> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
|
||||
|
||||
rtu_unit.uid = #units
|
||||
|
||||
-- determine hardware status
|
||||
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
rtu_unit.hw_state = RTU_HW_STATE.OFFLINE
|
||||
else
|
||||
if rtu_unit.is_multiblock then
|
||||
rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED)
|
||||
elseif faulted then
|
||||
rtu_unit.hw_state = RTU_HW_STATE.FAULTED
|
||||
else
|
||||
rtu_unit.hw_state = RTU_HW_STATE.OK
|
||||
end
|
||||
end
|
||||
|
||||
-- report hardware status
|
||||
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return true
|
||||
end
|
||||
@@ -1,10 +1,19 @@
|
||||
--
|
||||
-- Communications
|
||||
-- SCADA Network Communications Objects
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local insert = table.insert
|
||||
-- basic acceleration aliases
|
||||
|
||||
local type = type
|
||||
local insert = table.insert
|
||||
|
||||
local TYPE_NUM = "number"
|
||||
local TYPE_STR = "string"
|
||||
local TYPE_TBL = "table"
|
||||
|
||||
-- comms settings/attributes
|
||||
|
||||
---@type integer computer ID
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
@@ -17,9 +26,15 @@ local max_distance = nil
|
||||
local comms = {}
|
||||
|
||||
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
|
||||
comms.version = "3.0.8"
|
||||
comms.version = "3.1.0"
|
||||
comms.api_version = "0.0.10"
|
||||
|
||||
---@alias frame scada_frame|authd_frame
|
||||
---@alias packet_container modbus_container|rplc_container|mgmt_container|crdn_container
|
||||
---@alias packet modbus_adu|rplc_packet|mgmt_packet|crdn_packet
|
||||
|
||||
--#region Protocol Definitions
|
||||
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
MODBUS_TCP = 0, -- the "MODBUS TCP"-esque protocol
|
||||
@@ -46,16 +61,20 @@ local RPLC_TYPE = {
|
||||
|
||||
---@enum MGMT_TYPE
|
||||
local MGMT_TYPE = {
|
||||
-- connection
|
||||
ESTABLISH = 0, -- establish new connection
|
||||
KEEP_ALIVE = 1, -- keep alive packet w/ RTT
|
||||
CLOSE = 2, -- close a connection
|
||||
RTU_ADVERT = 3, -- RTU capability advertisement
|
||||
RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
|
||||
RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones
|
||||
DIAG_TONE_GET = 6, -- (API) diagnostic: get alarm tones
|
||||
DIAG_TONE_SET = 7, -- (API) diagnostic: set alarm tones
|
||||
DIAG_ALARM_SET = 8, -- (API) diagnostic: set alarm to simulate audio for
|
||||
INFO_LIST_CMP = 9 -- (API) info: list all computers on the network
|
||||
SWITCH_NET = 3,
|
||||
-- RTU
|
||||
RTU_ADVERT = 4, -- RTU capability advertisement
|
||||
RTU_DEV_REMOUNT = 5, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
|
||||
RTU_TONE_ALARM = 6, -- instruct RTUs to play specified alarm tones
|
||||
-- API
|
||||
DIAG_TONE_GET = 7, -- diagnostic - get alarm tones
|
||||
DIAG_TONE_SET = 8, -- diagnostic - set alarm tones
|
||||
DIAG_ALARM_SET = 9, -- diagnostic - set alarm to simulate audio for
|
||||
INFO_LIST_CMP = 10 -- info - list all computers on the network
|
||||
}
|
||||
|
||||
---@enum CRDN_TYPE
|
||||
@@ -135,11 +154,13 @@ comms.PLC_AUTO_ACK = PLC_AUTO_ACK
|
||||
comms.UNIT_COMMAND = UNIT_COMMAND
|
||||
comms.FAC_COMMAND = FAC_COMMAND
|
||||
|
||||
--#endregion
|
||||
|
||||
-- destination broadcast address (to all devices)
|
||||
comms.BROADCAST = -1
|
||||
|
||||
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet
|
||||
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame
|
||||
-- firmware version used to indicate an establish packet is a connection test
|
||||
comms.CONN_TEST_FWV = "CONN_TEST"
|
||||
|
||||
-- configure the maximum allowable message receive distance<br>
|
||||
-- packets received with distances greater than this will be silently discarded
|
||||
@@ -148,91 +169,172 @@ function comms.set_trusted_range(distance)
|
||||
if distance == 0 then max_distance = nil else max_distance = distance end
|
||||
end
|
||||
|
||||
-- generic SCADA packet
|
||||
--#region Network Frames (Layer 2)
|
||||
|
||||
-- SCADA link-layer discovery frame
|
||||
---@nodiscard
|
||||
function comms.scada_packet()
|
||||
function comms.lld_frame()
|
||||
local self = {
|
||||
modem_msg_in = nil, ---@type modem_message|nil
|
||||
modem_frame = nil, ---@type modem_frame|nil
|
||||
|
||||
valid = false,
|
||||
authenticated = false,
|
||||
|
||||
raw = {},
|
||||
src_addr = comms.BROADCAST,
|
||||
dest_addr = comms.BROADCAST,
|
||||
seq_num = -1,
|
||||
protocol = PROTOCOL.SCADA_MGMT,
|
||||
length = 0,
|
||||
payload = {}
|
||||
|
||||
src_addr = nil, ---@type integer|nil
|
||||
dest_addr = nil, ---@type integer|nil
|
||||
ack = false
|
||||
}
|
||||
|
||||
---@class scada_packet
|
||||
---@class lld_frame
|
||||
local public = {}
|
||||
|
||||
-- make a SCADA packet
|
||||
-- make a link-layer discovery frame
|
||||
---@param dest_addr integer destination computer address (ID)
|
||||
---@param ack boolean if this is an acknowledgement
|
||||
function public.make(dest_addr, ack)
|
||||
self.valid = true
|
||||
|
||||
self.src_addr = COMPUTER_ID
|
||||
self.dest_addr = dest_addr
|
||||
self.ack = ack
|
||||
|
||||
self.raw = { COMPUTER_ID, dest_addr, ack }
|
||||
end
|
||||
|
||||
-- parse in modem frame fields as a link-layer discovery frame
|
||||
---@param side string modem side
|
||||
---@param sender integer sender channel
|
||||
---@param reply_to integer reply channel
|
||||
---@param message any message body
|
||||
---@param distance integer transmission distance
|
||||
---@return boolean valid valid frame received
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
---@class modem_frame
|
||||
self.modem_frame = {
|
||||
iface = side, s_chan = sender, r_chan = reply_to, dist = distance, data = message
|
||||
}
|
||||
|
||||
self.valid = false
|
||||
self.raw = self.modem_frame.data
|
||||
|
||||
if (type(max_distance) == TYPE_NUM) and (type(distance) == TYPE_NUM) and (distance > max_distance) then
|
||||
-- outside of maximum allowable transmission distance
|
||||
-- log.debug("COMMS: lld_frame.receive(): discarding frame with distance " .. distance .. " (outside trusted range)")
|
||||
elseif type(self.raw) == TYPE_TBL then
|
||||
self.src_addr = self.raw[1]
|
||||
self.dest_addr = self.raw[2]
|
||||
self.ack = self.raw[3] == true
|
||||
|
||||
-- check if this frame is destined for this device, otherwise discard ASAP
|
||||
if (self.dest_addr == COMPUTER_ID) or (self.dest_addr == comms.BROADCAST) then
|
||||
self.valid = type(self.src_addr) == TYPE_NUM and type(self.dest_addr) == TYPE_NUM
|
||||
end
|
||||
end
|
||||
|
||||
return self.valid
|
||||
end
|
||||
|
||||
-- public accessors --
|
||||
|
||||
---@nodiscard
|
||||
function public.modem_event() return self.modem_frame end
|
||||
---@nodiscard
|
||||
function public.raw_frame() return self.raw end
|
||||
|
||||
---@nodiscard
|
||||
function public.interface() return self.modem_frame.iface end
|
||||
---@nodiscard
|
||||
function public.local_channel() return self.modem_frame.s_chan end
|
||||
---@nodiscard
|
||||
function public.remote_channel() return self.modem_frame.r_chan end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_valid() return self.valid end
|
||||
|
||||
---@nodiscard
|
||||
function public.src_addr() return self.src_addr or comms.BROADCAST end
|
||||
---@nodiscard
|
||||
function public.dest_addr() return self.dest_addr or comms.BROADCAST end
|
||||
---@nodiscard
|
||||
function public.is_ack() return self.ack end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- SCADA network frame
|
||||
---@nodiscard
|
||||
function comms.scada_frame()
|
||||
local self = {
|
||||
modem_frame = nil, ---@type modem_frame|nil
|
||||
|
||||
valid = false,
|
||||
authenticated = false,
|
||||
|
||||
raw = {},
|
||||
|
||||
src_addr = nil, ---@type integer|nil
|
||||
dest_addr = nil, ---@type integer|nil
|
||||
seq_num = nil, ---@type integer|nil
|
||||
protocol = nil, ---@type PROTOCOL|nil
|
||||
length = 0,
|
||||
payload = {}
|
||||
}
|
||||
|
||||
---@class scada_frame
|
||||
local public = {}
|
||||
|
||||
-- make a SCADA frame
|
||||
---@param dest_addr integer destination computer address (ID)
|
||||
---@param seq_num integer sequence number
|
||||
---@param protocol PROTOCOL
|
||||
---@param payload table
|
||||
function public.make(dest_addr, seq_num, protocol, payload)
|
||||
self.valid = true
|
||||
self.src_addr = COMPUTER_ID
|
||||
|
||||
self.src_addr = COMPUTER_ID
|
||||
self.dest_addr = dest_addr
|
||||
self.seq_num = seq_num
|
||||
self.protocol = protocol
|
||||
self.length = #payload
|
||||
self.payload = payload
|
||||
self.raw = { self.src_addr, self.dest_addr, self.seq_num, self.protocol, self.payload }
|
||||
self.seq_num = seq_num
|
||||
self.protocol = protocol
|
||||
self.length = #payload
|
||||
self.payload = payload
|
||||
|
||||
self.raw = { COMPUTER_ID, dest_addr, seq_num, protocol, payload }
|
||||
end
|
||||
|
||||
-- parse in a modem message as a SCADA packet
|
||||
-- parse in modem frame fields as a SCADA frame
|
||||
---@param side string modem side
|
||||
---@param sender integer sender channel
|
||||
---@param reply_to integer reply channel
|
||||
---@param message any message body
|
||||
---@param distance integer transmission distance
|
||||
---@return boolean valid valid message received
|
||||
---@return boolean valid valid frame received
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
---@class modem_message
|
||||
self.modem_msg_in = {
|
||||
iface = side,
|
||||
s_channel = sender,
|
||||
r_channel = reply_to,
|
||||
msg = message,
|
||||
dist = distance
|
||||
---@class modem_frame
|
||||
self.modem_frame = {
|
||||
iface = side, s_chan = sender, r_chan = reply_to, dist = distance, data = message
|
||||
}
|
||||
|
||||
self.valid = false
|
||||
self.raw = self.modem_msg_in.msg
|
||||
self.raw = self.modem_frame.data
|
||||
|
||||
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
|
||||
if (type(max_distance) == TYPE_NUM) and (type(distance) == TYPE_NUM) and (distance > max_distance) then
|
||||
-- outside of maximum allowable transmission distance
|
||||
-- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
else
|
||||
if type(self.raw) == "table" then
|
||||
if #self.raw == 5 then
|
||||
self.src_addr = self.raw[1]
|
||||
self.dest_addr = self.raw[2]
|
||||
self.seq_num = self.raw[3]
|
||||
self.protocol = self.raw[4]
|
||||
-- log.debug("COMMS: scada_frame.receive(): discarding frame with distance " .. distance .. " (outside trusted range)")
|
||||
elseif type(self.raw) == TYPE_TBL then
|
||||
self.src_addr = self.raw[1]
|
||||
self.dest_addr = self.raw[2]
|
||||
|
||||
-- element 5 must be a table
|
||||
if type(self.raw[5]) == "table" then
|
||||
self.length = #self.raw[5]
|
||||
self.payload = self.raw[5]
|
||||
end
|
||||
else
|
||||
self.src_addr = nil
|
||||
self.dest_addr = nil
|
||||
self.seq_num = nil
|
||||
self.protocol = nil
|
||||
self.length = 0
|
||||
self.payload = {}
|
||||
end
|
||||
-- check if this frame is destined for this device, otherwise discard ASAP
|
||||
-- if it is, check that the payload is a table and continue
|
||||
if ((self.dest_addr == COMPUTER_ID) or (self.dest_addr == comms.BROADCAST)) and (type(self.raw[5]) == TYPE_TBL) then
|
||||
self.seq_num = self.raw[3]
|
||||
self.protocol = self.raw[4]
|
||||
self.length = #self.raw[5]
|
||||
self.payload = self.raw[5]
|
||||
|
||||
-- check if this packet is destined for this device
|
||||
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
|
||||
|
||||
self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
|
||||
type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table"
|
||||
self.valid = type(self.src_addr) == TYPE_NUM and type(self.dest_addr) == TYPE_NUM and
|
||||
type(self.seq_num) == TYPE_NUM and type(self.protocol) == TYPE_NUM
|
||||
end
|
||||
end
|
||||
|
||||
@@ -245,16 +347,18 @@ function comms.scada_packet()
|
||||
-- public accessors --
|
||||
|
||||
---@nodiscard
|
||||
function public.modem_event() return self.modem_msg_in end
|
||||
function public.modem_event() return self.modem_frame end
|
||||
---@nodiscard
|
||||
function public.raw_header() return { self.src_addr, self.dest_addr, self.seq_num, self.protocol } end
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
function public.raw_frame() return self.raw end
|
||||
|
||||
---@nodiscard
|
||||
function public.local_channel() return self.modem_msg_in.s_channel end
|
||||
function public.interface() return self.modem_frame.iface end
|
||||
---@nodiscard
|
||||
function public.remote_channel() return self.modem_msg_in.r_channel end
|
||||
function public.local_channel() return self.modem_frame.s_chan end
|
||||
---@nodiscard
|
||||
function public.remote_channel() return self.modem_frame.r_chan end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_valid() return self.valid end
|
||||
@@ -262,90 +366,83 @@ function comms.scada_packet()
|
||||
function public.is_authenticated() return self.authenticated end
|
||||
|
||||
---@nodiscard
|
||||
function public.src_addr() return self.src_addr end
|
||||
function public.src_addr() return self.src_addr or comms.BROADCAST end
|
||||
---@nodiscard
|
||||
function public.dest_addr() return self.dest_addr end
|
||||
function public.dest_addr() return self.dest_addr or comms.BROADCAST end
|
||||
---@nodiscard
|
||||
function public.seq_num() return self.seq_num end
|
||||
function public.seq_num() return self.seq_num or -1 end
|
||||
---@nodiscard
|
||||
function public.protocol() return self.protocol end
|
||||
function public.protocol() return self.protocol or PROTOCOL.SCADA_MGMT end
|
||||
---@nodiscard
|
||||
function public.length() return self.length end
|
||||
function public.length() return self.length or 0 end
|
||||
---@nodiscard
|
||||
function public.data() return self.payload end
|
||||
function public.data() return self.payload or {} end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- authenticated SCADA packet
|
||||
-- authenticated SCADA frame
|
||||
---@nodiscard
|
||||
function comms.authd_packet()
|
||||
function comms.authd_frame()
|
||||
local self = {
|
||||
modem_msg_in = nil, ---@type modem_message|nil
|
||||
modem_frame = nil, ---@type modem_frame|nil
|
||||
|
||||
valid = false,
|
||||
|
||||
raw = {},
|
||||
src_addr = comms.BROADCAST,
|
||||
dest_addr = comms.BROADCAST,
|
||||
mac = "",
|
||||
payload = {}
|
||||
|
||||
src_addr = nil, ---@type integer|nil
|
||||
dest_addr = nil, ---@type integer|nil
|
||||
mac = "",
|
||||
payload = {}
|
||||
}
|
||||
|
||||
---@class authd_packet
|
||||
---@class authd_frame
|
||||
local public = {}
|
||||
|
||||
-- make an authenticated SCADA packet
|
||||
---@param s_packet scada_packet scada packet to authenticate
|
||||
-- make an authenticated SCADA frame
|
||||
---@param s_frame scada_frame scada frame to authenticate
|
||||
---@param mac function message authentication hash function
|
||||
function public.make(s_packet, mac)
|
||||
function public.make(s_frame, mac)
|
||||
self.valid = true
|
||||
self.src_addr = s_packet.src_addr()
|
||||
self.dest_addr = s_packet.dest_addr()
|
||||
self.mac = mac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
|
||||
self.raw = { self.src_addr, self.dest_addr, self.mac, s_packet.raw_sendable() }
|
||||
|
||||
self.src_addr = s_frame.src_addr()
|
||||
self.dest_addr = s_frame.dest_addr()
|
||||
self.mac = mac(textutils.serialize(s_frame.raw_header(), { allow_repetitions = true, compact = true }))
|
||||
|
||||
self.raw = { self.src_addr, self.dest_addr, self.mac, s_frame.raw_frame() }
|
||||
end
|
||||
|
||||
-- parse in a modem message as an authenticated SCADA packet
|
||||
-- parse in modem frame fields as an authenticated SCADA frame
|
||||
---@param side string modem side
|
||||
---@param sender integer sender channel
|
||||
---@param reply_to integer reply channel
|
||||
---@param message any message body
|
||||
---@param distance integer transmission distance
|
||||
---@return boolean valid valid message received
|
||||
---@return boolean valid valid frame received
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
---@class modem_message
|
||||
self.modem_msg_in = {
|
||||
iface = side,
|
||||
s_channel = sender,
|
||||
r_channel = reply_to,
|
||||
msg = message,
|
||||
dist = distance
|
||||
---@class modem_frame
|
||||
self.modem_frame = {
|
||||
iface = side, s_chan = sender, r_chan = reply_to, data = message, dist = distance
|
||||
}
|
||||
|
||||
self.valid = false
|
||||
self.raw = self.modem_msg_in.msg
|
||||
self.raw = self.modem_frame.data
|
||||
|
||||
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
|
||||
if (type(max_distance) == TYPE_NUM) and ((type(distance) ~= TYPE_NUM) or (distance > max_distance)) then
|
||||
-- outside of maximum allowable transmission distance
|
||||
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
else
|
||||
if type(self.raw) == "table" then
|
||||
if #self.raw == 4 then
|
||||
self.src_addr = self.raw[1]
|
||||
self.dest_addr = self.raw[2]
|
||||
self.mac = self.raw[3]
|
||||
self.payload = self.raw[4]
|
||||
else
|
||||
self.src_addr = nil
|
||||
self.dest_addr = nil
|
||||
self.mac = ""
|
||||
self.payload = {}
|
||||
end
|
||||
-- log.debug("COMMS: authd_frame.receive(): discarding frame with distance " .. distance .. " (outside trusted range)")
|
||||
elseif type(self.raw) == TYPE_TBL then
|
||||
self.src_addr = self.raw[1]
|
||||
self.dest_addr = self.raw[2]
|
||||
|
||||
-- check if this packet is destined for this device
|
||||
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
|
||||
-- check if this packet is destined for this device, otherwise discard ASAP
|
||||
if (self.dest_addr == COMPUTER_ID) or (self.dest_addr == comms.BROADCAST) then
|
||||
self.mac = self.raw[3]
|
||||
self.payload = self.raw[4]
|
||||
|
||||
self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
|
||||
type(self.mac) == "string" and type(self.payload) == "table"
|
||||
self.valid = type(self.src_addr) == TYPE_NUM and type(self.dest_addr) == TYPE_NUM and
|
||||
type(self.mac) == TYPE_STR and type(self.payload) == TYPE_TBL
|
||||
end
|
||||
end
|
||||
|
||||
@@ -355,44 +452,50 @@ function comms.authd_packet()
|
||||
-- public accessors --
|
||||
|
||||
---@nodiscard
|
||||
function public.modem_event() return self.modem_msg_in end
|
||||
function public.modem_event() return self.modem_frame end
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
function public.raw_frame() return self.raw end
|
||||
|
||||
---@nodiscard
|
||||
function public.local_channel() return self.modem_msg_in.s_channel end
|
||||
function public.local_channel() return self.modem_frame.s_chan end
|
||||
---@nodiscard
|
||||
function public.remote_channel() return self.modem_msg_in.r_channel end
|
||||
function public.remote_channel() return self.modem_frame.r_chan end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_valid() return self.valid end
|
||||
|
||||
---@nodiscard
|
||||
function public.src_addr() return self.src_addr end
|
||||
function public.src_addr() return self.src_addr or comms.BROADCAST end
|
||||
---@nodiscard
|
||||
function public.dest_addr() return self.dest_addr end
|
||||
function public.dest_addr() return self.dest_addr or comms.BROADCAST end
|
||||
---@nodiscard
|
||||
function public.mac() return self.mac end
|
||||
function public.mac() return self.mac or "" end
|
||||
---@nodiscard
|
||||
function public.data() return self.payload end
|
||||
function public.data() return self.payload or {} end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- MODBUS packet, modeled after MODBUS TCP
|
||||
--#endregion
|
||||
|
||||
--#region Network Packets (Layer 3)
|
||||
|
||||
-- MODBUS packet container, modeled after MODBUS TCP
|
||||
---@nodiscard
|
||||
function comms.modbus_packet()
|
||||
function comms.modbus_container()
|
||||
local self = {
|
||||
frame = nil,
|
||||
frame = nil, ---@type scada_frame
|
||||
|
||||
raw = {},
|
||||
txn_id = -1,
|
||||
length = 0,
|
||||
unit_id = -1,
|
||||
func_code = 0x80,
|
||||
data = {}
|
||||
|
||||
txn_id = -1,
|
||||
length = 0,
|
||||
unit_id = -1,
|
||||
func_code = 0x80, ---@type MODBUS_FCODE
|
||||
data = {}
|
||||
}
|
||||
|
||||
---@class modbus_packet
|
||||
---@class modbus_container
|
||||
local public = {}
|
||||
|
||||
-- make a MODBUS packet
|
||||
@@ -400,59 +503,61 @@ function comms.modbus_packet()
|
||||
---@param unit_id integer
|
||||
---@param func_code MODBUS_FCODE
|
||||
---@param data table
|
||||
---@return boolean success
|
||||
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
|
||||
if type(data) == TYPE_TBL then
|
||||
self.txn_id = txn_id
|
||||
self.length = #data
|
||||
self.unit_id = unit_id
|
||||
self.func_code = func_code
|
||||
self.data = data
|
||||
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 a table")
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
log.error("COMMS: [modbus_make] data not a table")
|
||||
return false
|
||||
end
|
||||
|
||||
-- decode a MODBUS packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
---@param frame scada_frame
|
||||
---@return modbus_adu|nil packet the decoded packet, if valid
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
local data = frame.data()
|
||||
|
||||
self.frame = frame
|
||||
self.raw = data
|
||||
|
||||
if frame.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
local size_ok = frame.length() >= 3
|
||||
self.txn_id = data[1]
|
||||
self.unit_id = data[2]
|
||||
self.func_code = data[3]
|
||||
self.data = { table.unpack(data, 4, #data) }
|
||||
self.length = #self.data
|
||||
|
||||
if size_ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) })
|
||||
if type(self.txn_id) == TYPE_NUM and type(self.unit_id) == TYPE_NUM and type(self.func_code) == TYPE_NUM then
|
||||
return public.get()
|
||||
end
|
||||
else log.debug("COMMS: [modbus_decode] attempted parse of incorrect protocol " .. frame.protocol(), true) end
|
||||
else log.debug("COMMS: [modbus_decode] discarding nil frame", true) 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
|
||||
return nil
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
-- get the raw packet table for transmission
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
function public.raw_packet() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
-- create a new packet (ADU) from this container's contents
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class modbus_frame
|
||||
local frame = {
|
||||
---@class modbus_adu
|
||||
local adu = {
|
||||
scada_frame = self.frame,
|
||||
txn_id = self.txn_id,
|
||||
length = self.length,
|
||||
@@ -461,84 +566,87 @@ function comms.modbus_packet()
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
return adu
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- reactor PLC packet
|
||||
-- reactor PLC packet container
|
||||
---@nodiscard
|
||||
function comms.rplc_packet()
|
||||
function comms.rplc_container()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = {},
|
||||
id = 0,
|
||||
type = 0, ---@type RPLC_TYPE
|
||||
frame = nil, ---@type scada_frame
|
||||
|
||||
raw = {},
|
||||
|
||||
id = 0,
|
||||
type = 0, ---@type RPLC_TYPE
|
||||
length = 0,
|
||||
data = {}
|
||||
data = {}
|
||||
}
|
||||
|
||||
---@class rplc_packet
|
||||
---@class rplc_container
|
||||
local public = {}
|
||||
|
||||
-- make an RPLC packet
|
||||
---@param id integer
|
||||
---@param packet_type RPLC_TYPE
|
||||
---@param data table
|
||||
---@return boolean success
|
||||
function public.make(id, packet_type, data)
|
||||
if type(data) == "table" then
|
||||
if type(data) == TYPE_TBL then
|
||||
-- packet accessor properties
|
||||
self.id = id
|
||||
self.type = packet_type
|
||||
self.id = id
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = 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 a table")
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
log.error("COMMS: [rplc_make] data not a table")
|
||||
return false
|
||||
end
|
||||
|
||||
-- decode an RPLC packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
---@param frame scada_frame
|
||||
---@return rplc_packet|nil packet the decoded packet, if valid
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
local data = frame.data()
|
||||
|
||||
self.frame = frame
|
||||
self.raw = data
|
||||
|
||||
if frame.protocol() == PROTOCOL.RPLC then
|
||||
local ok = frame.length() >= 2
|
||||
self.id = data[1]
|
||||
self.type = data[2]
|
||||
self.data = { table.unpack(data, 3, #data) }
|
||||
self.length = #self.data
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], data[2], { table.unpack(data, 3, #data) })
|
||||
if type(self.id) == TYPE_NUM and type(self.type) == TYPE_NUM then
|
||||
return public.get()
|
||||
end
|
||||
else log.debug("COMMS: [rplc_decode] attempted parse of incorrect protocol " .. frame.protocol(), true) end
|
||||
else log.debug("COMMS: [rplc_decode] nil frame encountered", true) 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
|
||||
return nil
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
-- get the raw packet table for transmission
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
function public.raw_packet() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
-- create a new packet from this container's contents
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class rplc_frame
|
||||
local frame = {
|
||||
---@class rplc_packet
|
||||
local packet = {
|
||||
scada_frame = self.frame,
|
||||
id = self.id,
|
||||
type = self.type,
|
||||
@@ -546,168 +654,178 @@ function comms.rplc_packet()
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
return packet
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- SCADA management packet
|
||||
-- SCADA management packet container
|
||||
---@nodiscard
|
||||
function comms.mgmt_packet()
|
||||
function comms.mgmt_container()
|
||||
local self = {
|
||||
frame = nil,
|
||||
frame = nil, ---@type scada_frame
|
||||
|
||||
raw = {},
|
||||
type = 0, ---@type MGMT_TYPE
|
||||
|
||||
type = 0, ---@type MGMT_TYPE
|
||||
length = 0,
|
||||
data = {}
|
||||
data = {}
|
||||
}
|
||||
|
||||
---@class mgmt_packet
|
||||
---@class mgmt_container
|
||||
local public = {}
|
||||
|
||||
-- make a SCADA management packet
|
||||
---@param packet_type MGMT_TYPE
|
||||
---@param data table
|
||||
---@return boolean success
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
if type(data) == TYPE_TBL then
|
||||
-- packet accessor properties
|
||||
self.type = packet_type
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = 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 a table")
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
log.error("COMMS: [mgmt_make] data not a table")
|
||||
return false
|
||||
end
|
||||
|
||||
-- decode a SCADA management packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
---@param frame scada_frame
|
||||
---@return mgmt_packet|nil packet the decoded packet, if valid
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
local data = frame.data()
|
||||
|
||||
self.frame = frame
|
||||
self.raw = data
|
||||
|
||||
if frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local ok = frame.length() >= 1
|
||||
self.type = data[1]
|
||||
self.data = { table.unpack(data, 2, #data) }
|
||||
self.length = #self.data
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], { table.unpack(data, 2, #data) })
|
||||
if type(self.type) == TYPE_NUM then
|
||||
return public.get()
|
||||
end
|
||||
else log.debug("COMMS: [mgmt_decode] attempted parse of incorrect protocol " .. frame.protocol(), true) end
|
||||
else log.debug("COMMS: [mgmt_decode] nil frame encountered", true) 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
|
||||
return nil
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
-- get the raw packet table for transmission
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
function public.raw_packet() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
-- create a new packet from this container's contents
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class mgmt_frame
|
||||
local frame = {
|
||||
---@class mgmt_packet
|
||||
local packet = {
|
||||
scada_frame = self.frame,
|
||||
type = self.type,
|
||||
length = self.length,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
return packet
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- SCADA coordinator packet
|
||||
-- SCADA coordinator packet container
|
||||
---@nodiscard
|
||||
function comms.crdn_packet()
|
||||
function comms.crdn_container()
|
||||
local self = {
|
||||
frame = nil,
|
||||
frame = nil, ---@type scada_frame
|
||||
|
||||
raw = {},
|
||||
type = 0, ---@type CRDN_TYPE
|
||||
|
||||
type = 0, ---@type CRDN_TYPE
|
||||
length = 0,
|
||||
data = {}
|
||||
data = {}
|
||||
}
|
||||
|
||||
---@class crdn_packet
|
||||
---@class crdn_container
|
||||
local public = {}
|
||||
|
||||
-- make a coordinator packet
|
||||
---@param packet_type CRDN_TYPE
|
||||
---@param data table
|
||||
---@return boolean success
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
if type(data) == TYPE_TBL then
|
||||
-- packet accessor properties
|
||||
self.type = packet_type
|
||||
self.type = packet_type
|
||||
self.length = #data
|
||||
self.data = 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 a table")
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
log.error("COMMS: [crdn_make] data not a table")
|
||||
return false
|
||||
end
|
||||
|
||||
-- decode a coordinator packet from a SCADA frame
|
||||
---@param frame scada_packet
|
||||
---@return boolean success
|
||||
---@param frame scada_frame
|
||||
---@return crdn_packet|nil packet the decoded packet, if valid
|
||||
function public.decode(frame)
|
||||
if frame then
|
||||
local data = frame.data()
|
||||
|
||||
self.frame = frame
|
||||
self.raw = data
|
||||
|
||||
if frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local ok = frame.length() >= 1
|
||||
self.type = data[1]
|
||||
self.data = { table.unpack(data, 2, #data) }
|
||||
self.length = #self.data
|
||||
|
||||
if ok then
|
||||
local data = frame.data()
|
||||
public.make(data[1], { table.unpack(data, 2, #data) })
|
||||
if type(self.type) == TYPE_NUM then
|
||||
return public.get()
|
||||
end
|
||||
else log.debug("COMMS: [crdn_decode] attempted parse of incorrect protocol " .. frame.protocol(), true) end
|
||||
else log.debug("COMMS: [crdn_decode] nil frame encountered", true) 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
|
||||
return nil
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
-- get the raw packet table for transmission
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
function public.raw_packet() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
-- create a new packet from this container's contents
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class crdn_frame
|
||||
local frame = {
|
||||
---@class crdn_packet
|
||||
local packet = {
|
||||
scada_frame = self.frame,
|
||||
type = self.type,
|
||||
length = self.length,
|
||||
data = self.data
|
||||
}
|
||||
|
||||
return frame
|
||||
return packet
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return comms
|
||||
|
||||
@@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 }
|
||||
|
||||
log.MODE = MODE
|
||||
|
||||
local logger = {
|
||||
local _log = {
|
||||
not_ready = true,
|
||||
path = "/log.txt",
|
||||
mode = MODE.APPEND,
|
||||
@@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace
|
||||
---@param err_msg string|nil error message
|
||||
---@return boolean out_of_space
|
||||
local function check_out_of_space(err_msg)
|
||||
return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
|
||||
return (free_space(_log.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
|
||||
end
|
||||
|
||||
-- private log write function
|
||||
---@param msg_bits any[]
|
||||
local function _log(msg_bits)
|
||||
if logger.not_ready then return end
|
||||
local function write_log(msg_bits)
|
||||
if _log.not_ready then return end
|
||||
|
||||
local time_stamp = os.date(TIME_FMT)
|
||||
local stamped = util.c(time_stamp, table.unpack(msg_bits))
|
||||
|
||||
-- attempt to write log
|
||||
local status, result = pcall(function ()
|
||||
logger.file.writeLine(stamped)
|
||||
logger.file.flush()
|
||||
_log.file.writeLine(stamped)
|
||||
_log.file.flush()
|
||||
end)
|
||||
|
||||
-- if we don't have space, we need to create a new log file
|
||||
if check_out_of_space() then
|
||||
-- delete the old log file before opening a new one
|
||||
logger.file.close()
|
||||
fs.delete(logger.path)
|
||||
_log.file.close()
|
||||
fs.delete(_log.path)
|
||||
|
||||
-- re-init logger and pass dmesg_out so that it doesn't change
|
||||
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
|
||||
log.init(_log.path, _log.mode, _log.debug, _log.dmesg_out)
|
||||
|
||||
-- log the message and recycle warning
|
||||
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
|
||||
logger.file.writeLine(stamped)
|
||||
logger.file.flush()
|
||||
_log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
|
||||
_log.file.writeLine(stamped)
|
||||
_log.file.flush()
|
||||
elseif (not status) and (result ~= nil) then
|
||||
util.println("unexpected error writing to the log file: " .. result)
|
||||
end
|
||||
@@ -89,45 +89,45 @@ end
|
||||
function log.init(path, write_mode, include_debug, dmesg_redirect)
|
||||
local err_msg
|
||||
|
||||
logger.path = path
|
||||
logger.mode = write_mode
|
||||
logger.debug = include_debug
|
||||
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
|
||||
_log.path = path
|
||||
_log.mode = write_mode
|
||||
_log.debug = include_debug
|
||||
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
|
||||
|
||||
if dmesg_redirect then
|
||||
logger.dmesg_out = dmesg_redirect
|
||||
_log.dmesg_out = dmesg_redirect
|
||||
else
|
||||
logger.dmesg_out = term.current()
|
||||
_log.dmesg_out = term.current()
|
||||
end
|
||||
|
||||
-- check for space issues
|
||||
local out_of_space = check_out_of_space(err_msg)
|
||||
|
||||
-- try to handle problems
|
||||
if logger.file == nil or out_of_space then
|
||||
if _log.file == nil or out_of_space then
|
||||
if out_of_space then
|
||||
if fs.exists(logger.path) then
|
||||
fs.delete(logger.path)
|
||||
if fs.exists(_log.path) then
|
||||
fs.delete(_log.path)
|
||||
|
||||
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
|
||||
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
|
||||
|
||||
if logger.file then
|
||||
logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
|
||||
logger.file.flush()
|
||||
if _log.file then
|
||||
_log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
|
||||
_log.file.flush()
|
||||
else error("failed to setup the log file: " .. err_msg) end
|
||||
else error("failed to make space for the log file, please delete unused files") end
|
||||
else error("unexpected error setting up the log file: " .. err_msg) end
|
||||
end
|
||||
|
||||
logger.not_ready = false
|
||||
_log.not_ready = false
|
||||
end
|
||||
|
||||
-- close the log file handle
|
||||
function log.close() logger.file.close() end
|
||||
function log.close() _log.file.close() end
|
||||
|
||||
-- direct dmesg output to a monitor/window
|
||||
---@param window Window window or terminal reference
|
||||
function log.direct_dmesg(window) logger.dmesg_out = window end
|
||||
function log.direct_dmesg(window) _log.dmesg_out = window end
|
||||
|
||||
-- dmesg style logging for boot because I like linux-y things
|
||||
---@param msg any message
|
||||
@@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
tag = util.strval(tag or "")
|
||||
|
||||
local t_stamp = string.format("%12.2f", os.clock())
|
||||
local out = logger.dmesg_out
|
||||
local out = _log.dmesg_out
|
||||
|
||||
if out ~= nil then
|
||||
local out_w, out_h = out.getSize()
|
||||
@@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
||||
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
@@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
||||
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
@@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color)
|
||||
out.write(lines[i])
|
||||
end
|
||||
|
||||
logger.dmesg_restore_coord = { out.getCursorPos() }
|
||||
_log.dmesg_restore_coord = { out.getCursorPos() }
|
||||
|
||||
_log{"[", t_stamp, "] [", tag, "] ", msg}
|
||||
write_log{"[", t_stamp, "] [", tag, "] ", msg}
|
||||
end
|
||||
|
||||
return ts_coord
|
||||
@@ -241,9 +241,9 @@ end
|
||||
---@return function update, function done
|
||||
function log.dmesg_working(msg, tag, tag_color)
|
||||
local ts_coord = log.dmesg(msg, tag, tag_color)
|
||||
local initial_scroll = logger.dmesg_scroll_count
|
||||
local initial_scroll = _log.dmesg_scroll_count
|
||||
|
||||
local out = logger.dmesg_out
|
||||
local out = _log.dmesg_out
|
||||
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
||||
|
||||
if out ~= nil then
|
||||
@@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
local counter = 0
|
||||
|
||||
local function update(sec_remaining)
|
||||
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
||||
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
|
||||
if new_y < 1 then return end
|
||||
|
||||
local time = util.sprintf("%ds", sec_remaining)
|
||||
@@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
||||
out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
|
||||
end
|
||||
|
||||
local function done(ok)
|
||||
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
||||
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
|
||||
if new_y < 1 then return end
|
||||
|
||||
out.setCursorPos(ts_coord.x1, new_y)
|
||||
@@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
|
||||
out.setTextColor(initial_color)
|
||||
|
||||
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
||||
out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
|
||||
end
|
||||
|
||||
return update, done
|
||||
@@ -312,28 +312,28 @@ end
|
||||
---@param msg any message
|
||||
---@param trace? boolean include file trace
|
||||
function log.debug(msg, trace)
|
||||
if logger.debug then
|
||||
if _log.debug then
|
||||
if trace then
|
||||
local info = debug.getinfo(2)
|
||||
|
||||
if info.name ~= nil then
|
||||
_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
write_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
else
|
||||
_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
write_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
end
|
||||
else
|
||||
_log{DBG_TAG, msg}
|
||||
write_log{DBG_TAG, msg}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- log info messages
|
||||
---@param msg any message
|
||||
function log.info(msg) _log{INF_TAG, msg} end
|
||||
function log.info(msg) write_log{INF_TAG, msg} end
|
||||
|
||||
-- log warning messages
|
||||
---@param msg any message
|
||||
function log.warning(msg) _log{WRN_TAG, msg} end
|
||||
function log.warning(msg) write_log{WRN_TAG, msg} end
|
||||
|
||||
-- log error messages
|
||||
---@param msg any message
|
||||
@@ -343,17 +343,17 @@ function log.error(msg, trace)
|
||||
local info = debug.getinfo(2)
|
||||
|
||||
if info.name ~= nil then
|
||||
_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
write_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
else
|
||||
_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
write_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
end
|
||||
else
|
||||
_log{ERR_TAG, msg}
|
||||
write_log{ERR_TAG, msg}
|
||||
end
|
||||
end
|
||||
|
||||
-- log fatal errors
|
||||
---@param msg any message
|
||||
function log.fatal(msg) _log{FTL_TAG, msg} end
|
||||
function log.fatal(msg) write_log{FTL_TAG, msg} end
|
||||
|
||||
return log
|
||||
|
||||
@@ -16,7 +16,7 @@ local mqueue = {}
|
||||
local TYPE = {
|
||||
COMMAND = 0,
|
||||
DATA = 1,
|
||||
PACKET = 2
|
||||
NETWORK = 2
|
||||
}
|
||||
|
||||
mqueue.TYPE = TYPE
|
||||
@@ -60,9 +60,9 @@ function mqueue.new()
|
||||
---@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 packet|frame
|
||||
function public.push_packet(packet) _push(TYPE.PACKET, packet) end
|
||||
-- push a SCADA frame, packet, or packet container onto the queue
|
||||
---@param object frame|packet|packet_container
|
||||
function public.push_network(object) _push(TYPE.NETWORK, object) end
|
||||
|
||||
-- get an item off the queue
|
||||
---@nodiscard
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
--
|
||||
-- Network Communications
|
||||
-- Network Communications and Message Authentication
|
||||
--
|
||||
|
||||
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 md5 = require("lockbox.digest.md5")
|
||||
@@ -13,11 +14,15 @@ local hmac = require("lockbox.mac.hmac")
|
||||
local stream = require("lockbox.util.stream")
|
||||
local array = require("lockbox.util.array")
|
||||
|
||||
local LINK_TIMEOUT_MS = 5000
|
||||
local DISCOVERY_PERIOD_UP_MS = 3000
|
||||
local DISCOVERY_PERIOD_DOWN_MS = 500
|
||||
|
||||
---@class scada_net_interface
|
||||
local network = {}
|
||||
|
||||
-- cryptography engine
|
||||
local c_eng = {
|
||||
local _crypt = {
|
||||
key = nil,
|
||||
hmac = nil
|
||||
}
|
||||
@@ -39,23 +44,23 @@ function network.init_mac(passkey)
|
||||
key_deriv.setPassword(passkey)
|
||||
key_deriv.finish()
|
||||
|
||||
c_eng.key = array.fromHex(key_deriv.asHex())
|
||||
_crypt.key = array.fromHex(key_deriv.asHex())
|
||||
|
||||
-- initialize HMAC
|
||||
c_eng.hmac = hmac()
|
||||
c_eng.hmac.setBlockSize(64)
|
||||
c_eng.hmac.setDigest(md5)
|
||||
c_eng.hmac.setKey(c_eng.key)
|
||||
_crypt.hmac = hmac()
|
||||
_crypt.hmac.setBlockSize(64)
|
||||
_crypt.hmac.setDigest(md5)
|
||||
_crypt.hmac.setKey(_crypt.key)
|
||||
|
||||
local init_time = util.time_ms() - start
|
||||
log.info("network.init_mac completed in " .. init_time .. "ms")
|
||||
log.info("NET: network.init_mac() completed in " .. init_time .. "ms")
|
||||
|
||||
return init_time
|
||||
end
|
||||
|
||||
-- de-initialize message authentication system
|
||||
function network.deinit_mac()
|
||||
c_eng.key, c_eng.hmac = nil, nil
|
||||
_crypt.key, _crypt.hmac = nil, nil
|
||||
end
|
||||
|
||||
-- generate HMAC of message
|
||||
@@ -64,40 +69,88 @@ end
|
||||
local function compute_hmac(message)
|
||||
-- local start = util.time_ms()
|
||||
|
||||
c_eng.hmac.init()
|
||||
c_eng.hmac.update(stream.fromString(message))
|
||||
c_eng.hmac.finish()
|
||||
_crypt.hmac.init()
|
||||
_crypt.hmac.update(stream.fromString(message))
|
||||
_crypt.hmac.finish()
|
||||
|
||||
local hash = c_eng.hmac.asHex()
|
||||
local hash = _crypt.hmac.asHex()
|
||||
|
||||
-- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
|
||||
-- log.debug("NET: compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
|
||||
|
||||
return hash
|
||||
end
|
||||
|
||||
-- NIC: Network Interface Controller<br>
|
||||
-- utilizes HMAC-MD5 for message authentication, if enabled
|
||||
---@param modem Modem modem to use
|
||||
function network.nic(modem)
|
||||
-- Network Interface Controller (NIC)<br>
|
||||
-- sends and receives network frames using a modem<br>
|
||||
-- utilizes HMAC-MD5 for message authentication, if enabled and using a wireless modem
|
||||
---@param modem Modem|nil modem to use
|
||||
---@param lld_tx_chan integer? link layer discovery transmit channel, or nil to disable probing (will still reply)
|
||||
function network.nic(modem, lld_tx_chan)
|
||||
local self = {
|
||||
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
|
||||
-- modem interface name
|
||||
iface = "?",
|
||||
-- phy name
|
||||
name = "?",
|
||||
-- used to avoid costly MAC calculations if not required
|
||||
use_hash = false,
|
||||
-- used to quickly return out of tx/rx functions if there is nothing to do
|
||||
phy_up = false,
|
||||
-- monitor if this NIC appears to have network access
|
||||
link_up = false,
|
||||
-- last time a discovery reply or other traffic was received
|
||||
last_lld_rx = 0,
|
||||
-- last time of discovery transmit
|
||||
last_lld_tx = 0,
|
||||
-- open channels
|
||||
channels = {}
|
||||
}
|
||||
|
||||
-- send a link-layer discovery frame
|
||||
---@param dest_addr integer destination address
|
||||
---@param r_chan integer remote channel
|
||||
---@param l_chan integer local channel
|
||||
---@param ack boolean true if this is an acknowledgement
|
||||
local function _send_ll_discovery_frame(dest_addr, r_chan, l_chan, ack)
|
||||
if not self.phy_up then return end
|
||||
|
||||
local reply = comms.lld_frame()
|
||||
|
||||
reply.make(dest_addr, ack)
|
||||
|
||||
-- log.debug(util.c("_send_ll_discovery_frame { ", dest_addr, ", ", r_chan, ", ", l_chan, " }"))
|
||||
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
modem.transmit(r_chan, l_chan, reply.raw_frame())
|
||||
end
|
||||
|
||||
---@class nic:Modem
|
||||
local public = {}
|
||||
|
||||
-- get the phy name
|
||||
---@nodiscard
|
||||
function public.phy_name() return self.name end
|
||||
|
||||
-- check if this NIC has a connected modem
|
||||
---@nodiscard
|
||||
function public.is_connected() return self.connected end
|
||||
function public.is_connected() return self.phy_up end
|
||||
|
||||
-- check if this NIC detected a network link
|
||||
---@nodiscard
|
||||
function public.is_network_up() return self.link_up end
|
||||
|
||||
-- connect to a modem peripheral
|
||||
---@param reconnected_modem Modem
|
||||
function public.connect(reconnected_modem)
|
||||
modem = reconnected_modem
|
||||
self.connected = true
|
||||
|
||||
-- open previously opened channels
|
||||
self.iface = ppm.get_iface(modem)
|
||||
self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}")
|
||||
self.use_hash = _crypt.hmac and modem.isWireless()
|
||||
self.phy_up = true
|
||||
self.link_up = false
|
||||
|
||||
-- open only previously opened channels
|
||||
modem.closeAll()
|
||||
for _, channel in ipairs(self.channels) do
|
||||
modem.open(channel)
|
||||
end
|
||||
@@ -109,7 +162,10 @@ function network.nic(modem)
|
||||
end
|
||||
|
||||
-- flag this NIC as no longer having a connected modem (usually do to peripheral disconnect)
|
||||
function public.disconnect() self.connected = false end
|
||||
function public.disconnect()
|
||||
self.phy_up = false
|
||||
self.link_up = false
|
||||
end
|
||||
|
||||
-- check if a peripheral is this modem
|
||||
---@nodiscard
|
||||
@@ -117,13 +173,13 @@ function network.nic(modem)
|
||||
function public.is_modem(device) return device == modem end
|
||||
|
||||
-- wrap modem functions, then create custom functions
|
||||
public.connect(modem)
|
||||
if modem then public.connect(modem) end
|
||||
|
||||
-- open a channel on the modem<br>
|
||||
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
|
||||
---@param channel integer
|
||||
function public.open(channel)
|
||||
modem.open(channel)
|
||||
if modem then modem.open(channel) end
|
||||
|
||||
local already_open = false
|
||||
for i = 1, #self.channels do
|
||||
@@ -141,7 +197,7 @@ function network.nic(modem)
|
||||
-- close a channel on the modem
|
||||
---@param channel integer
|
||||
function public.close(channel)
|
||||
modem.close(channel)
|
||||
if modem then modem.close(channel) end
|
||||
|
||||
for i = 1, #self.channels do
|
||||
if self.channels[i] == channel then
|
||||
@@ -153,75 +209,115 @@ function network.nic(modem)
|
||||
|
||||
-- close all channels on the modem
|
||||
function public.closeAll()
|
||||
modem.closeAll()
|
||||
if modem then modem.closeAll() end
|
||||
self.channels = {}
|
||||
end
|
||||
|
||||
-- send a packet, with message authentication if configured
|
||||
-- send a frame, with message authentication if configured
|
||||
---@param dest_channel integer destination channel
|
||||
---@param local_channel integer local channel
|
||||
---@param packet scada_packet packet
|
||||
function public.transmit(dest_channel, local_channel, packet)
|
||||
if self.connected then
|
||||
local tx_packet = packet ---@type authd_packet|scada_packet
|
||||
---@param frame scada_frame frame
|
||||
function public.transmit(dest_channel, local_channel, frame)
|
||||
if self.phy_up then
|
||||
local tx_frame = frame ---@type authd_frame|scada_frame
|
||||
|
||||
if c_eng.hmac ~= nil then
|
||||
if self.use_hash then
|
||||
-- local start = util.time_ms()
|
||||
tx_packet = comms.authd_packet()
|
||||
|
||||
---@cast tx_packet authd_packet
|
||||
tx_packet.make(packet, compute_hmac)
|
||||
|
||||
-- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
|
||||
tx_frame = comms.authd_frame()
|
||||
tx_frame.make(frame, compute_hmac)
|
||||
-- log.debug("NET: network.nic.transmit(): data processing took " .. (util.time_ms() - start) .. "ms")
|
||||
end
|
||||
|
||||
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
modem.transmit(dest_channel, local_channel, tx_frame.raw_frame())
|
||||
else
|
||||
log.debug("NET: " .. self.name ..".transmit() tx dropped, phy is down")
|
||||
end
|
||||
end
|
||||
|
||||
-- parse in a modem message as a network packet
|
||||
-- parse in modem frame components as a SCADA network frame
|
||||
---@nodiscard
|
||||
---@param side string modem side
|
||||
---@param sender integer sender channel
|
||||
---@param reply_to integer reply channel
|
||||
---@param message any packet sent with or without message authentication
|
||||
---@param message any SCADA frame sent with or without message authentication
|
||||
---@param distance integer transmission distance
|
||||
---@return scada_packet|nil packet received packet if valid and passed authentication check
|
||||
---@return scada_frame|nil frame received frame if valid and passed authentication check
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
local packet = nil
|
||||
local frame = nil
|
||||
|
||||
if self.connected then
|
||||
local s_packet = comms.scada_packet()
|
||||
if self.phy_up and side == self.iface then
|
||||
local s_frame = comms.scada_frame()
|
||||
|
||||
if c_eng.hmac ~= nil then
|
||||
-- parse packet as an authenticated SCADA packet
|
||||
local a_packet = comms.authd_packet()
|
||||
a_packet.receive(side, sender, reply_to, message, distance)
|
||||
if self.use_hash then
|
||||
-- parse frame as an authenticated SCADA frame
|
||||
local a_frame = comms.authd_frame()
|
||||
|
||||
if a_packet.is_valid() then
|
||||
s_packet.receive(side, sender, reply_to, a_packet.data(), distance)
|
||||
|
||||
if s_packet.is_valid() then
|
||||
if a_frame.receive(side, sender, reply_to, message, distance) then
|
||||
if s_frame.receive(side, sender, reply_to, a_frame.data(), distance) then
|
||||
-- local start = util.time_ms()
|
||||
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
|
||||
local computed_hmac = compute_hmac(textutils.serialize(s_frame.raw_header(), { allow_repetitions = true, compact = true }))
|
||||
|
||||
if a_packet.mac() == computed_hmac then
|
||||
-- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
||||
s_packet.stamp_authenticated()
|
||||
if a_frame.mac() == computed_hmac then
|
||||
-- log.debug("NET: " .. self.name ..".receive(): HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
||||
s_frame.stamp_authenticated()
|
||||
else
|
||||
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
||||
-- log.debug("NET: " .. self.name ..".receive(): HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- parse packet as a generic SCADA packet
|
||||
s_packet.receive(side, sender, reply_to, message, distance)
|
||||
-- parse frame as a generic SCADA frame
|
||||
s_frame.receive(side, sender, reply_to, message, distance)
|
||||
end
|
||||
|
||||
if s_packet.is_valid() then packet = s_packet end
|
||||
-- if valid, return it, otherwise try to handle it as a link-layer transaction
|
||||
if s_frame.is_valid() then
|
||||
self.link_up = true
|
||||
self.last_lld_rx = util.time_ms()
|
||||
|
||||
frame = s_frame
|
||||
else
|
||||
local l_frame = comms.lld_frame()
|
||||
|
||||
-- try instead to receive this as a link-layer discovery frame, then respond if valid
|
||||
-- keep the returned value as nil; this is internal layer 2 logic to hide from the application
|
||||
if l_frame.receive(side, sender, reply_to, message, distance) then
|
||||
self.link_up = true
|
||||
self.last_lld_rx = util.time_ms()
|
||||
|
||||
if not l_frame.is_ack() then
|
||||
_send_ll_discovery_frame(l_frame.src_addr(), l_frame.remote_channel(), l_frame.local_channel(), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return packet
|
||||
return frame
|
||||
end
|
||||
|
||||
-- periodic NIC task to maintain network link detection
|
||||
---@return boolean link_up if the network link is still up
|
||||
function public.periodic()
|
||||
local now = util.time_ms()
|
||||
|
||||
if now >= (self.last_lld_rx + LINK_TIMEOUT_MS) then
|
||||
if self.link_up then log.debug("NET: " .. self.name ..".periodic(): link timeout") end
|
||||
|
||||
self.link_up = false
|
||||
end
|
||||
|
||||
if lld_tx_chan and self.phy_up then
|
||||
if (now - self.last_lld_tx) > util.trinary(self.link_up, DISCOVERY_PERIOD_UP_MS, DISCOVERY_PERIOD_DOWN_MS) then
|
||||
self.last_lld_tx = now
|
||||
|
||||
for _, channel in ipairs(self.channels) do
|
||||
_send_ll_discovery_frame(comms.BROADCAST, lld_tx_chan, channel, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self.link_up
|
||||
end
|
||||
|
||||
return public
|
||||
|
||||
@@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
|
||||
|
||||
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
|
||||
|
||||
local ppm_sys = {
|
||||
local _ppm = {
|
||||
mounts = {}, ---@type { [string]: ppm_entry }
|
||||
next_vid = 0,
|
||||
auto_cf = false,
|
||||
@@ -66,7 +66,7 @@ local function peri_init(iface)
|
||||
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
|
||||
if _ppm.auto_cf then _ppm.faulted = false end
|
||||
|
||||
self.fault_counts[key] = 0
|
||||
|
||||
@@ -78,10 +78,10 @@ local function peri_init(iface)
|
||||
self.faulted = true
|
||||
self.last_fault = result
|
||||
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = result
|
||||
_ppm.faulted = true
|
||||
_ppm.last_fault = result
|
||||
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
if not _ppm.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]"
|
||||
@@ -92,7 +92,7 @@ local function peri_init(iface)
|
||||
|
||||
self.fault_counts[key] = self.fault_counts[key] + 1
|
||||
|
||||
if result == "Terminated" then ppm_sys.terminate = true end
|
||||
if result == "Terminated" then _ppm.terminate = true end
|
||||
|
||||
return ACCESS_FAULT, result
|
||||
end
|
||||
@@ -159,10 +159,10 @@ local function peri_init(iface)
|
||||
self.faulted = true
|
||||
self.last_fault = UNDEFINED_FIELD
|
||||
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = UNDEFINED_FIELD
|
||||
_ppm.faulted = true
|
||||
_ppm.last_fault = UNDEFINED_FIELD
|
||||
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
if not _ppm.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 calls]"
|
||||
@@ -193,35 +193,35 @@ end
|
||||
-- REPORTING --
|
||||
|
||||
-- silence error prints
|
||||
function ppm.disable_reporting() ppm_sys.mute = true end
|
||||
function ppm.disable_reporting() _ppm.mute = true end
|
||||
|
||||
-- allow error prints
|
||||
function ppm.enable_reporting() ppm_sys.mute = false end
|
||||
function ppm.enable_reporting() _ppm.mute = false end
|
||||
|
||||
-- FAULT MEMORY --
|
||||
|
||||
-- enable automatically clearing fault flag
|
||||
function ppm.enable_afc() ppm_sys.auto_cf = true end
|
||||
function ppm.enable_afc() _ppm.auto_cf = true end
|
||||
|
||||
-- disable automatically clearing fault flag
|
||||
function ppm.disable_afc() ppm_sys.auto_cf = false end
|
||||
function ppm.disable_afc() _ppm.auto_cf = false end
|
||||
|
||||
-- clear fault flag
|
||||
function ppm.clear_fault() ppm_sys.faulted = false end
|
||||
function ppm.clear_fault() _ppm.faulted = false end
|
||||
|
||||
-- check fault flag
|
||||
---@nodiscard
|
||||
function ppm.is_faulted() return ppm_sys.faulted end
|
||||
function ppm.is_faulted() return _ppm.faulted end
|
||||
|
||||
-- get the last fault message
|
||||
---@nodiscard
|
||||
function ppm.get_last_fault() return ppm_sys.last_fault end
|
||||
function ppm.get_last_fault() return _ppm.last_fault end
|
||||
|
||||
-- TERMINATION --
|
||||
|
||||
-- if a caught error was a termination request
|
||||
---@nodiscard
|
||||
function ppm.should_terminate() return ppm_sys.terminate end
|
||||
function ppm.should_terminate() return _ppm.terminate end
|
||||
|
||||
-- MOUNTING --
|
||||
|
||||
@@ -229,12 +229,12 @@ function ppm.should_terminate() return ppm_sys.terminate end
|
||||
function ppm.mount_all()
|
||||
local ifaces = peripheral.getNames()
|
||||
|
||||
ppm_sys.mounts = {}
|
||||
_ppm.mounts = {}
|
||||
|
||||
for i = 1, #ifaces do
|
||||
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
_ppm.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
|
||||
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
log.info(util.c("PPM: found a ", _ppm.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
end
|
||||
|
||||
if #ifaces == 0 then
|
||||
@@ -253,10 +253,10 @@ function ppm.mount(iface)
|
||||
|
||||
for i = 1, #ifaces do
|
||||
if iface == ifaces[i] then
|
||||
ppm_sys.mounts[iface] = peri_init(iface)
|
||||
_ppm.mounts[iface] = peri_init(iface)
|
||||
|
||||
pm_type = ppm_sys.mounts[iface].type
|
||||
pm_dev = ppm_sys.mounts[iface].dev
|
||||
pm_type = _ppm.mounts[iface].type
|
||||
pm_dev = _ppm.mounts[iface].dev
|
||||
|
||||
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
|
||||
break
|
||||
@@ -278,12 +278,12 @@ function ppm.remount(iface)
|
||||
for i = 1, #ifaces do
|
||||
if iface == ifaces[i] then
|
||||
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type))
|
||||
ppm.unmount(ppm_sys.mounts[iface].dev)
|
||||
ppm.unmount(_ppm.mounts[iface].dev)
|
||||
|
||||
ppm_sys.mounts[iface] = peri_init(iface)
|
||||
_ppm.mounts[iface] = peri_init(iface)
|
||||
|
||||
pm_type = ppm_sys.mounts[iface].type
|
||||
pm_dev = ppm_sys.mounts[iface].dev
|
||||
pm_type = _ppm.mounts[iface].type
|
||||
pm_dev = _ppm.mounts[iface].dev
|
||||
|
||||
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
|
||||
break
|
||||
@@ -293,28 +293,28 @@ function ppm.remount(iface)
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
|
||||
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
|
||||
-- mount a virtual placeholder device
|
||||
---@nodiscard
|
||||
---@return string type, table device
|
||||
function ppm.mount_virtual()
|
||||
local iface = "ppm_vdev_" .. ppm_sys.next_vid
|
||||
local iface = "ppm_vdev_" .. _ppm.next_vid
|
||||
|
||||
ppm_sys.mounts[iface] = peri_init("__virtual__")
|
||||
ppm_sys.next_vid = ppm_sys.next_vid + 1
|
||||
_ppm.mounts[iface] = peri_init("__virtual__")
|
||||
_ppm.next_vid = _ppm.next_vid + 1
|
||||
|
||||
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
|
||||
|
||||
return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev
|
||||
return _ppm.mounts[iface].type, _ppm.mounts[iface].dev
|
||||
end
|
||||
|
||||
-- manually unmount a peripheral from the PPM
|
||||
---@param device table device table
|
||||
function ppm.unmount(device)
|
||||
if device then
|
||||
for iface, data in pairs(ppm_sys.mounts) do
|
||||
for iface, data in pairs(_ppm.mounts) do
|
||||
if data.dev == device then
|
||||
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
|
||||
ppm_sys.mounts[iface] = nil
|
||||
_ppm.mounts[iface] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -330,7 +330,7 @@ function ppm.handle_unmount(iface)
|
||||
local pm_type = nil
|
||||
|
||||
-- what got disconnected?
|
||||
local lost_dev = ppm_sys.mounts[iface]
|
||||
local lost_dev = _ppm.mounts[iface]
|
||||
|
||||
if lost_dev then
|
||||
pm_type = lost_dev.type
|
||||
@@ -341,18 +341,18 @@ function ppm.handle_unmount(iface)
|
||||
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
|
||||
end
|
||||
|
||||
ppm_sys.mounts[iface] = nil
|
||||
_ppm.mounts[iface] = nil
|
||||
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
|
||||
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
|
||||
function ppm.log_mounts()
|
||||
for iface, mount in pairs(ppm_sys.mounts) do
|
||||
for iface, mount in pairs(_ppm.mounts) do
|
||||
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
|
||||
end
|
||||
|
||||
if util.table_len(ppm_sys.mounts) == 0 then
|
||||
if util.table_len(_ppm.mounts) == 0 then
|
||||
log.warning("PPM: no devices had been found")
|
||||
end
|
||||
end
|
||||
@@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end
|
||||
---@return { [string]: ppm_entry } mounts
|
||||
function ppm.list_mounts()
|
||||
local list = {}
|
||||
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
|
||||
for k, v in pairs(_ppm.mounts) do list[k] = v end
|
||||
return list
|
||||
end
|
||||
|
||||
@@ -379,7 +379,7 @@ end
|
||||
---@return string|nil iface CC peripheral interface
|
||||
function ppm.get_iface(device)
|
||||
if device then
|
||||
for iface, data in pairs(ppm_sys.mounts) do
|
||||
for iface, data in pairs(_ppm.mounts) do
|
||||
if data.dev == device then return iface end
|
||||
end
|
||||
end
|
||||
@@ -392,8 +392,8 @@ end
|
||||
---@param iface string CC peripheral interface
|
||||
---@return { [string]: function }|nil device function table
|
||||
function ppm.get_periph(iface)
|
||||
if ppm_sys.mounts[iface] then
|
||||
return ppm_sys.mounts[iface].dev
|
||||
if _ppm.mounts[iface] then
|
||||
return _ppm.mounts[iface].dev
|
||||
else return nil end
|
||||
end
|
||||
|
||||
@@ -402,20 +402,20 @@ end
|
||||
---@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
|
||||
if _ppm.mounts[iface] then
|
||||
return _ppm.mounts[iface].type
|
||||
else return nil end
|
||||
end
|
||||
|
||||
-- get all mounted peripherals by type
|
||||
---@nodiscard
|
||||
---@param name string type name
|
||||
---@param type string type name
|
||||
---@return table devices device function tables
|
||||
function ppm.get_all_devices(name)
|
||||
function ppm.get_all_devices(type)
|
||||
local devices = {}
|
||||
|
||||
for _, data in pairs(ppm_sys.mounts) do
|
||||
if data.type == name then
|
||||
for _, data in pairs(_ppm.mounts) do
|
||||
if data.type == type then
|
||||
table.insert(devices, data.dev)
|
||||
end
|
||||
end
|
||||
@@ -425,44 +425,72 @@ end
|
||||
|
||||
-- get a mounted peripheral by type (if multiple, returns the first)
|
||||
---@nodiscard
|
||||
---@param name string type name
|
||||
---@return table|nil device function table
|
||||
function ppm.get_device(name)
|
||||
local device = nil
|
||||
---@param type string type name
|
||||
---@return table|nil device, string|nil iface device and interface
|
||||
function ppm.get_device(type)
|
||||
local device, d_iface = nil, nil
|
||||
|
||||
for _, data in pairs(ppm_sys.mounts) do
|
||||
if data.type == name then
|
||||
for iface, data in pairs(_ppm.mounts) do
|
||||
if data.type == type then
|
||||
device = data.dev
|
||||
d_iface = iface
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return device
|
||||
return device, d_iface
|
||||
end
|
||||
|
||||
-- SPECIFIC DEVICE ACCESSORS --
|
||||
|
||||
-- get the fission reactor (if multiple, returns the first)
|
||||
---@nodiscard
|
||||
---@return table|nil reactor function table
|
||||
---@return table|nil reactor, string|nil iface reactor and interface
|
||||
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
|
||||
|
||||
-- get a modem by name
|
||||
---@nodiscard
|
||||
---@param iface string CC peripheral interface
|
||||
---@return Modem|nil modem function table
|
||||
function ppm.get_modem(iface)
|
||||
local modem = nil
|
||||
local device = _ppm.mounts[iface]
|
||||
|
||||
if device and device.type == "modem" then modem = device.dev end
|
||||
|
||||
return modem
|
||||
end
|
||||
|
||||
-- get the wireless modem (if multiple, returns the first)<br>
|
||||
-- if this is in a CraftOS emulated environment, wired modems will be used instead
|
||||
---@nodiscard
|
||||
---@return Modem|nil modem function table
|
||||
---@return Modem|nil modem, string|nil iface
|
||||
function ppm.get_wireless_modem()
|
||||
local w_modem = nil
|
||||
local w_modem, w_iface = nil, nil
|
||||
local emulated_env = periphemu ~= nil
|
||||
|
||||
for _, device in pairs(ppm_sys.mounts) do
|
||||
for iface, device in pairs(_ppm.mounts) do
|
||||
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
|
||||
w_modem = device.dev
|
||||
w_iface = iface
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return w_modem
|
||||
return w_modem, w_iface
|
||||
end
|
||||
|
||||
-- list all connected wired modems
|
||||
---@nodiscard
|
||||
---@return { [string]: ppm_entry } modems
|
||||
function ppm.get_wired_modem_list()
|
||||
local list = {}
|
||||
|
||||
for iface, device in pairs(_ppm.mounts) do
|
||||
if device.type == "modem" and not device.dev.isWireless() then list[iface] = device end
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
-- list all connected monitors
|
||||
@@ -471,7 +499,7 @@ end
|
||||
function ppm.get_monitor_list()
|
||||
local list = {}
|
||||
|
||||
for iface, device in pairs(ppm_sys.mounts) do
|
||||
for iface, device in pairs(_ppm.mounts) do
|
||||
if device.type == "monitor" then list[iface] = device end
|
||||
end
|
||||
|
||||
|
||||
@@ -210,8 +210,17 @@ end
|
||||
|
||||
---@alias color integer
|
||||
|
||||
---@alias auto_ctl_cfg [ PROCESS, number, integer, integer, number, number, number[] ]
|
||||
|
||||
--#region ENUMERATION TYPES
|
||||
|
||||
---@enum LISTEN_MODE
|
||||
types.LISTEN_MODE = {
|
||||
WIRELESS = 1,
|
||||
WIRED = 2,
|
||||
ALL = 3
|
||||
}
|
||||
|
||||
---@enum TEMP_SCALE
|
||||
types.TEMP_SCALE = {
|
||||
KELVIN = 1,
|
||||
@@ -385,9 +394,10 @@ types.PROCESS = {
|
||||
BURN_RATE = 2,
|
||||
CHARGE = 3,
|
||||
GEN_RATE = 4,
|
||||
MATRIX_FAULT_IDLE = 5,
|
||||
SYSTEM_ALARM_IDLE = 6,
|
||||
GEN_RATE_FAULT_IDLE = 7
|
||||
RANGE_CONTROL = 5,
|
||||
MATRIX_FAULT_IDLE = 6,
|
||||
SYSTEM_ALARM_IDLE = 7,
|
||||
GEN_RATE_FAULT_IDLE = 8
|
||||
}
|
||||
|
||||
types.PROCESS_NAMES = {
|
||||
@@ -396,6 +406,7 @@ types.PROCESS_NAMES = {
|
||||
"BURN_RATE",
|
||||
"CHARGE",
|
||||
"GEN_RATE",
|
||||
"RANGE_CONTROL",
|
||||
"MATRIX_FAULT_IDLE",
|
||||
"SYSTEM_ALARM_IDLE",
|
||||
"GEN_RATE_FAULT_IDLE"
|
||||
@@ -561,7 +572,7 @@ types.ALARM_STATE_NAMES = {
|
||||
---| "websocket_failure"
|
||||
---| "websocket_message"
|
||||
---| "websocket_success"
|
||||
---| "clock_start" (custom)
|
||||
---| "conn_test_complete" (custom)
|
||||
|
||||
---@alias fluid
|
||||
---| "mekanism:empty_gas"
|
||||
|
||||
@@ -24,7 +24,7 @@ local t_pack = table.pack
|
||||
local util = {}
|
||||
|
||||
-- scada-common version
|
||||
util.version = "1.5.4"
|
||||
util.version = "1.8.2"
|
||||
|
||||
util.TICK_TIME_S = 0.05
|
||||
util.TICK_TIME_MS = 50
|
||||
@@ -199,7 +199,7 @@ function util.sign(x) return util.trinary(x < 0, -1, 1) end
|
||||
---@return integer rounded
|
||||
function util.round(x) return math.floor(x + 0.5) end
|
||||
|
||||
-- get a new moving average object
|
||||
-- get a new moving average object (FIR filter)
|
||||
---@nodiscard
|
||||
---@param length integer history length
|
||||
function util.mov_avg(length)
|
||||
|
||||
172
supervisor/backplane.lua
Normal file
172
supervisor/backplane.lua
Normal file
@@ -0,0 +1,172 @@
|
||||
--
|
||||
-- Supervisor System Core Peripheral Backplane
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("supervisor.databus")
|
||||
|
||||
local println = util.println
|
||||
|
||||
---@class supervisor_backplane
|
||||
local backplane = {}
|
||||
|
||||
local _bp = {
|
||||
config = nil, ---@type svr_config
|
||||
lan_iface = false, ---@type string|false wired comms modem name
|
||||
|
||||
wd_nic = nil, ---@type nic|nil wired nic
|
||||
wl_nic = nil, ---@type nic|nil wireless nic
|
||||
nic_map = {} ---@type nic[] connected nics
|
||||
}
|
||||
|
||||
-- network interfaces indexed by peripheral names
|
||||
backplane.nics = _bp.nic_map
|
||||
|
||||
-- initialize the system peripheral backplane
|
||||
---@param config svr_config
|
||||
---@return boolean success
|
||||
function backplane.init(config)
|
||||
_bp.lan_iface = config.WiredModem
|
||||
|
||||
-- setup the wired modem, if configured
|
||||
if type(_bp.lan_iface) == "string" then
|
||||
local modem = ppm.get_modem(_bp.lan_iface)
|
||||
local wd_nic = network.nic(modem)
|
||||
|
||||
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
|
||||
|
||||
if not (modem and _bp.lan_iface) then
|
||||
println("startup> wired comms modem not found")
|
||||
log.fatal("BKPLN: no wired comms modem on startup")
|
||||
return false
|
||||
end
|
||||
|
||||
_bp.wd_nic = wd_nic
|
||||
_bp.nic_map[_bp.lan_iface] = wd_nic
|
||||
|
||||
wd_nic.closeAll()
|
||||
wd_nic.open(config.SVR_Channel)
|
||||
|
||||
databus.tx_hw_wd_modem(true)
|
||||
end
|
||||
|
||||
-- setup the wireless modem, if configured
|
||||
if config.WirelessModem then
|
||||
local modem, iface = ppm.get_wireless_modem()
|
||||
local wl_nic = network.nic(modem)
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
|
||||
|
||||
if not (modem and iface) then
|
||||
println("startup> wireless comms modem not found")
|
||||
log.fatal("BKPLN: no wireless comms modem on startup")
|
||||
return false
|
||||
end
|
||||
|
||||
_bp.wl_nic = wl_nic
|
||||
_bp.nic_map[iface] = wl_nic
|
||||
|
||||
wl_nic.closeAll()
|
||||
wl_nic.open(config.SVR_Channel)
|
||||
|
||||
databus.tx_hw_wl_modem(true)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral attach
|
||||
---@param iface string
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param print_no_fp function
|
||||
function backplane.attach(iface, type, device, print_no_fp)
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
local m_is_wl = device.isWireless()
|
||||
|
||||
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
|
||||
|
||||
if _bp.wd_nic and (_bp.lan_iface == iface) then
|
||||
-- connect this as the wired NIC
|
||||
_bp.wd_nic.connect(device)
|
||||
_bp.nic_map[iface] = _bp.wd_nic
|
||||
|
||||
log.info("BKPLN: WIRED PHY_UP " .. iface)
|
||||
print_no_fp("wired comms modem reconnected")
|
||||
|
||||
databus.tx_hw_wd_modem(true)
|
||||
elseif _bp.wl_nic and (not _bp.wl_nic.is_connected()) and m_is_wl then
|
||||
-- connect this as the wireless NIC
|
||||
_bp.wl_nic.connect(device)
|
||||
_bp.nic_map[iface] = _bp.wl_nic
|
||||
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
|
||||
print_no_fp("wireless comms modem reconnected")
|
||||
|
||||
databus.tx_hw_wl_modem(true)
|
||||
elseif _bp.wl_nic and m_is_wl then
|
||||
-- the wireless NIC already has a modem
|
||||
device.closeAll()
|
||||
|
||||
print_no_fp("standby wireless modem connected")
|
||||
log.info("BKPLN: standby wireless modem connected")
|
||||
else
|
||||
device.closeAll()
|
||||
|
||||
print_no_fp("unassigned modem connected")
|
||||
log.warning("BKPLN: unassigned modem connected")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a backplane peripheral detach
|
||||
---@param iface string
|
||||
---@param type string
|
||||
---@param device table
|
||||
---@param print_no_fp function
|
||||
function backplane.detach(iface, type, device, print_no_fp)
|
||||
if type == "modem" then
|
||||
---@cast device Modem
|
||||
|
||||
log.info(util.c("BKPLN: PHY_DETACH ", iface))
|
||||
|
||||
_bp.nic_map[iface] = nil
|
||||
|
||||
if _bp.wd_nic and _bp.wd_nic.is_modem(device) then
|
||||
_bp.wd_nic.disconnect()
|
||||
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
|
||||
|
||||
print_no_fp("wired modem disconnected")
|
||||
log.warning("BKPLN: wired comms modem disconnected")
|
||||
|
||||
databus.tx_hw_wd_modem(false)
|
||||
elseif _bp.wl_nic and _bp.wl_nic.is_modem(device) then
|
||||
_bp.wl_nic.disconnect()
|
||||
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
|
||||
|
||||
print_no_fp("wireless comms modem disconnected")
|
||||
log.warning("BKPLN: wireless comms modem disconnected")
|
||||
|
||||
local modem, m_iface = ppm.get_wireless_modem()
|
||||
if modem then
|
||||
log.info("BKPLN: found another wireless modem, using it for comms")
|
||||
|
||||
_bp.wl_nic.connect(modem)
|
||||
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
|
||||
else
|
||||
databus.tx_hw_wl_modem(false)
|
||||
end
|
||||
else
|
||||
print_no_fp("unassigned modem disconnected")
|
||||
log.warning("BKPLN: unassigned modem disconnected")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return backplane
|
||||
@@ -18,8 +18,6 @@ local tri = util.trinary
|
||||
local cpair = core.cpair
|
||||
|
||||
local self = {
|
||||
tank_fluid_opts = {}, ---@type Radio2D[]
|
||||
|
||||
vis_draw = nil, ---@type function
|
||||
draw_fluid_ops = nil, ---@type function
|
||||
|
||||
@@ -187,16 +185,16 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
local fac_c_8 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
local fac_c_9 = Div{parent=fac_cfg,x=2,y=4,width=49}
|
||||
|
||||
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7,fac_c_8,fac_c_9}}
|
||||
local fac_pane = MultiPane{parent=fac_cfg,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7,fac_c_8,fac_c_9}}
|
||||
|
||||
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
|
||||
TextBox{parent=fac_cfg,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
--#region Unit Count
|
||||
|
||||
TextBox{parent=fac_c_1,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
|
||||
tool_ctl.num_units = NumberField{parent=fac_c_1,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=fac_c_1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
|
||||
tool_ctl.num_units = NumberField{parent=fac_c_1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=fac_c_1,x=7,y=5,text="reactors"}
|
||||
TextBox{parent=fac_c_1,x=1,y=7,height=3,text="If you already configured your coordinator, make sure you update the coordinator's configured unit count.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
TextBox{parent=fac_c_1,y=7,height=3,text="If you already configured your coordinator, make sure you update the coordinator's configured unit count.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
|
||||
|
||||
local nu_error = TextBox{parent=fac_c_1,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
@@ -223,14 +221,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
else nu_error.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Cooling Configuration
|
||||
|
||||
TextBox{parent=fac_c_2,x=1,y=1,height=4,text="Please provide the reactor cooling configuration below. This includes the number of turbines, boilers, and if that reactor has a connection to a dynamic tank for emergency coolant."}
|
||||
TextBox{parent=fac_c_2,x=1,y=6,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=fac_c_2,y=1,height=4,text="Please provide the reactor cooling configuration below. This includes the number of turbines, boilers, and if that reactor has a connection to a dynamic tank for emergency coolant."}
|
||||
TextBox{parent=fac_c_2,y=6,text="UNIT TURBINES BOILERS HAS TANK CONNECTION?",fg_bg=g_lg_fg_bg}
|
||||
|
||||
for i = 1, 4 do
|
||||
local num_t, num_b, has_t = 1, 0, false
|
||||
@@ -242,7 +240,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
has_t = conf.TankConnection == true
|
||||
end
|
||||
|
||||
local line = Div{parent=fac_c_2,x=1,y=7+i,height=1}
|
||||
local line = Div{parent=fac_c_2,y=7+i,height=1}
|
||||
|
||||
TextBox{parent=line,text="Unit "..i,width=6}
|
||||
local turbines = NumberField{parent=line,x=9,y=1,width=5,max_chars=2,default=num_t,min=1,max=3,fg_bg=bw_fg_bg}
|
||||
@@ -306,16 +304,16 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_2,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_cooling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Facility Tanks Option
|
||||
|
||||
TextBox{parent=fac_c_3,x=1,y=1,height=6,text="You have set one or more of your units to use dynamic tanks for emergency coolant. You have two paths for configuration. The first is to assign dynamic tanks to reactor units; one tank per reactor, only connected to that reactor. RTU configurations must also assign it as such."}
|
||||
TextBox{parent=fac_c_3,x=1,y=8,height=3,text="Alternatively, you can configure them as facility tanks to connect to multiple reactor units. These can intermingle with unit-specific tanks."}
|
||||
TextBox{parent=fac_c_3,y=1,height=6,text="You have set one or more of your units to use dynamic tanks for emergency coolant. You have two paths for configuration. The first is to assign dynamic tanks to reactor units; one tank per reactor, only connected to that reactor. RTU configurations must also assign it as such."}
|
||||
TextBox{parent=fac_c_3,y=8,height=3,text="Alternatively, you can configure them as facility tanks to connect to multiple reactor units. These can intermingle with unit-specific tanks."}
|
||||
|
||||
tool_ctl.en_fac_tanks = Checkbox{parent=fac_c_3,x=1,y=12,label="Use Facility Dynamic Tanks",default=ini_cfg.FacilityTankMode>0,box_fg_bg=cpair(colors.yellow,colors.black)}
|
||||
tool_ctl.en_fac_tanks = Checkbox{parent=fac_c_3,y=12,label="Use Facility Dynamic Tanks",default=ini_cfg.FacilityTankMode>0,box_fg_bg=cpair(colors.yellow,colors.black)}
|
||||
|
||||
local function submit_en_fac_tank()
|
||||
if tool_ctl.en_fac_tanks.get_value() then
|
||||
@@ -338,19 +336,19 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_3,y=14,text="\x1b Back",callback=function()fac_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_fac_tank,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Facility Tank Connections
|
||||
|
||||
TextBox{parent=fac_c_4,x=1,y=1,height=4,text="Please set unit connections to dynamic tanks, selecting at least one facility tank. The layout for facility tanks will be configured next."}
|
||||
TextBox{parent=fac_c_4,y=1,height=4,text="Please set unit connections to dynamic tanks, selecting at least one facility tank. The layout for facility tanks will be configured next."}
|
||||
|
||||
for i = 1, 4 do
|
||||
local val = math.max(1, ini_cfg.FacilityTankDefs[i] or 2)
|
||||
local div = Div{parent=fac_c_4,x=1,y=3+(2*i),height=2}
|
||||
local div = Div{parent=fac_c_4,y=3+(2*i),height=2}
|
||||
|
||||
TextBox{parent=div,x=1,y=1,width=33,text="Unit "..i.." will be connected to..."}
|
||||
TextBox{parent=div,y=1,width=33,text="Unit "..i.." will be connected to..."}
|
||||
TextBox{parent=div,x=6,y=2,width=3,text="..."}
|
||||
local tank_opt = Radio2D{parent=div,x=9,y=2,rows=1,columns=2,default=val,options={"its own Unit Tank","a Facility Tank"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
local no_tank = TextBox{parent=div,x=9,y=2,width=34,text="no tank (as you set two steps ago)",fg_bg=cpair(colors.gray,colors.lightGray),hidden=true}
|
||||
@@ -404,13 +402,13 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
else tank_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_4,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_4,y=14,text="\x1b Back",callback=function()fac_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_4,x=44,y=14,text="Next \x1a",callback=submit_tank_defs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Facility Tank Mode
|
||||
|
||||
TextBox{parent=fac_c_5,x=1,y=1,text="Please select your dynamic tank layout."}
|
||||
TextBox{parent=fac_c_5,y=1,text="Please select your dynamic tank layout."}
|
||||
TextBox{parent=fac_c_5,x=12,y=3,text="Facility Tanks Unit Tanks",fg_bg=g_lg_fg_bg}
|
||||
|
||||
--#region Tank Layout Visualizer
|
||||
@@ -431,18 +429,18 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
|
||||
-- draw facility tank connections
|
||||
|
||||
local ftank_1 = Div{parent=vis,x=1,y=1,width=13,height=1}
|
||||
local ftank_1 = Div{parent=vis,y=1,width=13,height=1}
|
||||
TextBox{parent=ftank_1,width=7,text="Tank F1"}
|
||||
self.vis_ftanks[1] = {
|
||||
line = ftank_1, pipe_direct = TextBox{parent=ftank_1,x=9,y=1,width=5,text=string.rep("\x8c",5),fg_bg=pipe_cpair}
|
||||
}
|
||||
|
||||
for i = 2, 4 do
|
||||
local line = Div{parent=vis,x=1,y=(i-1)*2,width=13,height=2}
|
||||
local line = Div{parent=vis,y=(i-1)*2,width=13,height=2}
|
||||
local pipe_conn = TextBox{parent=line,x=13,y=2,width=1,text="\x8c",fg_bg=pipe_cpair}
|
||||
local pipe_chain = TextBox{parent=line,x=12,y=1,width=1,height=2,text="\x95\n\x8d",fg_bg=pipe_cpair}
|
||||
local pipe_direct = TextBox{parent=line,x=9,y=2,width=4,text="\x8c\x8c\x8c\x8c",fg_bg=pipe_cpair}
|
||||
local label = TextBox{parent=line,x=1,y=2,width=7,text=""}
|
||||
local label = TextBox{parent=line,y=2,width=7,text=""}
|
||||
self.vis_ftanks[i] = { line = line, pipe_conn = pipe_conn, pipe_chain = pipe_chain, pipe_direct = pipe_direct, label = label }
|
||||
end
|
||||
|
||||
@@ -574,7 +572,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
end
|
||||
|
||||
local tank_modes = { "Mode 1", "Mode 2", "Mode 3", "Mode 4", "Mode 5", "Mode 6", "Mode 7", "Mode 8" }
|
||||
tool_ctl.tank_mode = RadioButton{parent=fac_c_5,x=1,y=4,callback=change_mode,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow}
|
||||
tool_ctl.tank_mode = RadioButton{parent=fac_c_5,y=4,callback=change_mode,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -587,7 +585,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
fac_pane.set_value(7)
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_5,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_5,y=14,text="\x1b Back",callback=function()fac_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_5,x=44,y=14,text="Next \x1a",callback=next_from_tank_mode,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
PushButton{parent=fac_c_5,x=8,y=14,min_width=7,text="About",callback=function()fac_pane.set_value(6)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg}
|
||||
@@ -599,14 +597,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
TextBox{parent=fac_c_6,y=5,height=4,text="Examples: A U2 tank should be configured on an RTU as the dynamic tank for unit #2. An F3 tank should be configured on an RTU as the #3 dynamic tank for the facility."}
|
||||
TextBox{parent=fac_c_6,y=10,height=3,text="Some modes may look the same if you are not using 4 total reactor units. The wiki has details. Modes that look the same will function the same.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
PushButton{parent=fac_c_6,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_6,y=14,text="\x1b Back",callback=function()fac_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
--#region Dynamic Tank Fluid Types
|
||||
|
||||
TextBox{parent=fac_c_7,height=3,text="Specify each tank's coolant type, for display use only. Water is the only option if one or more of the connected units is water cooled."}
|
||||
|
||||
local tank_fluid_list = Div{parent=fac_c_7,x=1,y=5,height=8}
|
||||
local tank_fluid_list = Div{parent=fac_c_7,y=5,height=8}
|
||||
|
||||
function self.draw_fluid_ops()
|
||||
tank_fluid_list.remove_all()
|
||||
@@ -621,7 +619,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
|
||||
if type == 0 then type = 1 end
|
||||
|
||||
self.tank_fluid_opts[i] = nil
|
||||
tool_ctl.tank_fluid_opts[i] = nil
|
||||
|
||||
if tank_list[i] == 1 then
|
||||
local row = Div{parent=tank_fluid_list,height=2}
|
||||
@@ -636,7 +634,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
tank_fluid.disable()
|
||||
end
|
||||
|
||||
self.tank_fluid_opts[i] = tank_fluid
|
||||
tool_ctl.tank_fluid_opts[i] = tank_fluid
|
||||
elseif tank_list[i] == 2 then
|
||||
local row = Div{parent=tank_fluid_list,height=2}
|
||||
|
||||
@@ -661,7 +659,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
tank_fluid.disable()
|
||||
end
|
||||
|
||||
self.tank_fluid_opts[i] = tank_fluid
|
||||
tool_ctl.tank_fluid_opts[i] = tank_fluid
|
||||
|
||||
next_f = next_f + 1
|
||||
end
|
||||
@@ -676,17 +674,15 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
tmp_cfg.TankFluidTypes = {}
|
||||
|
||||
for i = 1, #tmp_cfg.FacilityTankList do
|
||||
if self.tank_fluid_opts[i] ~= nil then
|
||||
tmp_cfg.TankFluidTypes[i] = self.tank_fluid_opts[i].get_value()
|
||||
else
|
||||
tmp_cfg.TankFluidTypes[i] = 0
|
||||
end
|
||||
if tool_ctl.tank_fluid_opts[i] ~= nil then
|
||||
tmp_cfg.TankFluidTypes[i] = tool_ctl.tank_fluid_opts[i].get_value()
|
||||
else tmp_cfg.TankFluidTypes[i] = 0 end
|
||||
end
|
||||
|
||||
fac_pane.set_value(8)
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_7,y=14,text="\x1b Back",callback=back_from_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_tank_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -695,7 +691,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
TextBox{parent=fac_c_8,height=5,text="Auxiliary water coolant can be enabled for units to provide extra water during turbine ramp-up. For water cooled reactors, this goes to the reactor. For sodium cooled reactors, water goes to the boiler."}
|
||||
|
||||
for i = 1, 4 do
|
||||
local line = Div{parent=fac_c_8,x=1,y=7+i,height=1}
|
||||
local line = Div{parent=fac_c_8,y=7+i,height=1}
|
||||
|
||||
TextBox{parent=line,text="Unit "..i.." -",width=8}
|
||||
local aux_cool = Checkbox{parent=line,x=10,y=1,label="Has Auxiliary Coolant",default=ini_cfg.AuxiliaryCoolant[i],box_fg_bg=cpair(colors.yellow,colors.black)}
|
||||
@@ -713,7 +709,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
fac_pane.set_value(9)
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_8,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_8,y=14,text="\x1b Back",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_8,x=44,y=14,text="Next \x1a",callback=submit_aux_cool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -722,14 +718,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
|
||||
TextBox{parent=fac_c_9,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."}
|
||||
TextBox{parent=fac_c_9,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."}
|
||||
|
||||
local ext_idling = Checkbox{parent=fac_c_9,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
|
||||
local ext_idling = Checkbox{parent=fac_c_9,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
|
||||
|
||||
local function submit_idling()
|
||||
tmp_cfg.ExtChargeIdling = ext_idling.get_value()
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=fac_c_9,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_9,y=14,text="\x1b Back",callback=function()fac_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=fac_c_9,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
@@ -14,6 +15,7 @@ local TextBox = require("graphics.elements.TextBox")
|
||||
|
||||
local Checkbox = require("graphics.elements.controls.Checkbox")
|
||||
local PushButton = require("graphics.elements.controls.PushButton")
|
||||
local Radio2D = require("graphics.elements.controls.Radio2D")
|
||||
local RadioButton = require("graphics.elements.controls.RadioButton")
|
||||
|
||||
local NumberField = require("graphics.elements.form.NumberField")
|
||||
@@ -25,14 +27,22 @@ local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LISTEN_MODE = types.LISTEN_MODE
|
||||
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
local self = {
|
||||
importing_legacy = false,
|
||||
|
||||
update_net_cfg = nil, ---@type function
|
||||
show_auth_key = nil, ---@type function
|
||||
|
||||
pkt_test = nil, ---@type Checkbox
|
||||
pkt_chan = nil, ---@type NumberField
|
||||
pkt_timeout = nil, ---@type NumberField
|
||||
show_key_btn = nil, ---@type PushButton
|
||||
auth_key_textbox = nil, ---@type TextBox
|
||||
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
@@ -62,115 +72,230 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_5 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
local net_c_6 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
local net_pane = MultiPane{parent=net_cfg,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4,net_c_5,net_c_6}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
TextBox{parent=net_cfg,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_1,y=1,text="Please select the network interface(s)."}
|
||||
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local function on_wired_change(_) tool_ctl.gen_modem_list() end
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"}
|
||||
local plc_chan = NumberField{parent=net_c_1,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local wireless = Checkbox{parent=net_c_1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black)}
|
||||
TextBox{parent=net_c_1,x=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
|
||||
local wired = Checkbox{parent=net_c_1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
|
||||
TextBox{parent=net_c_1,x=3,y=6,text="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
|
||||
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",fg_bg=g_lg_fg_bg}
|
||||
local modem_list = ListBox{parent=net_c_1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"}
|
||||
local rtu_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
local function submit_interfaces()
|
||||
tmp_cfg.WirelessModem = wireless.get_value()
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
if not wired.get_value() then
|
||||
tmp_cfg.WiredModem = false
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
if not (wired.get_value() or wireless.get_value()) then
|
||||
modem_err.set_value("Please select a modem type.")
|
||||
modem_err.show()
|
||||
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
|
||||
modem_err.set_value("Please select a wired modem.")
|
||||
modem_err.show()
|
||||
else
|
||||
self.update_net_cfg()
|
||||
net_pane.set_value(2)
|
||||
modem_err.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,y=1,text="Please assign device connection interfaces if you selected multiple network interfaces."}
|
||||
TextBox{parent=net_c_2,x=39,y=2,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
TextBox{parent=net_c_2,y=4,text="Reactor PLC\nRTU Gateway\nCoordinator",fg_bg=g_lg_fg_bg}
|
||||
local opts = { "Wireless", "Wired", "Both" }
|
||||
local plc_listen = Radio2D{parent=net_c_2,x=14,y=4,rows=1,columns=3,default=ini_cfg.PLC_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
local rtu_listen = Radio2D{parent=net_c_2,x=14,rows=1,columns=3,default=ini_cfg.RTU_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
local crd_listen = Radio2D{parent=net_c_2,x=14,rows=1,columns=3,default=ini_cfg.CRD_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||
|
||||
local function on_pocket_en(en)
|
||||
if not en then
|
||||
self.pkt_test.set_value(false)
|
||||
self.pkt_test.disable()
|
||||
else self.pkt_test.enable() end
|
||||
end
|
||||
|
||||
TextBox{parent=net_c_2,y=8,text="With a wireless modem, configure Pocket access."}
|
||||
local pkt_en = Checkbox{parent=net_c_2,y=10,label="Enable Pocket Access",default=ini_cfg.PocketEnabled,callback=on_pocket_en,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=24,y=10,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
self.pkt_test = Checkbox{parent=net_c_2,label="Enable Pocket Remote System Testing",default=ini_cfg.PocketTest,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_2,x=39,y=11,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
|
||||
TextBox{parent=net_c_2,x=3,text="This allows remotely playing alarm sounds.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local function submit_net_cfg_opts()
|
||||
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
|
||||
tmp_cfg.PLC_Listen = plc_listen.get_value()
|
||||
tmp_cfg.RTU_Listen = rtu_listen.get_value()
|
||||
tmp_cfg.CRD_Listen = crd_listen.get_value()
|
||||
else
|
||||
if tmp_cfg.WiredModem then
|
||||
tmp_cfg.PLC_Listen = LISTEN_MODE.WIRED
|
||||
tmp_cfg.RTU_Listen = LISTEN_MODE.WIRED
|
||||
tmp_cfg.CRD_Listen = LISTEN_MODE.WIRED
|
||||
else
|
||||
tmp_cfg.PLC_Listen = LISTEN_MODE.WIRELESS
|
||||
tmp_cfg.RTU_Listen = LISTEN_MODE.WIRELESS
|
||||
tmp_cfg.CRD_Listen = LISTEN_MODE.WIRELESS
|
||||
end
|
||||
end
|
||||
|
||||
if tmp_cfg.WirelessModem then
|
||||
tmp_cfg.PocketEnabled = pkt_en.get_value()
|
||||
tmp_cfg.PocketTest = self.pkt_test.get_value()
|
||||
else
|
||||
tmp_cfg.PocketEnabled = false
|
||||
tmp_cfg.PocketTest = false
|
||||
end
|
||||
|
||||
if tmp_cfg.PocketEnabled then
|
||||
self.pkt_chan.enable()
|
||||
self.pkt_timeout.enable()
|
||||
else
|
||||
self.pkt_chan.disable()
|
||||
self.pkt_timeout.disable()
|
||||
end
|
||||
|
||||
net_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_net_cfg_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=1,text="Please set the network channels below."}
|
||||
TextBox{parent=net_c_3,y=3,height=4,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=8,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_3,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=9,width=11,text="PLC Channel"}
|
||||
local plc_chan = NumberField{parent=net_c_3,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=10,width=19,text="RTU Gateway Channel"}
|
||||
local rtu_chan = NumberField{parent=net_c_3,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=11,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_3,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_3,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,y=12,width=14,text="Pocket Channel"}
|
||||
self.pkt_chan = NumberField{parent=net_c_3,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=net_c_3,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c, plc_c, rtu_c = tonumber(svr_chan.get_value()), tonumber(plc_chan.get_value()), tonumber(rtu_chan.get_value())
|
||||
local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||
local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(self.pkt_chan.get_value())
|
||||
|
||||
if not tmp_cfg.PocketEnabled then pkt_c = tmp_cfg.PKT_Channel or 16244 end
|
||||
|
||||
if svr_c ~= nil and plc_c ~= nil and rtu_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
|
||||
tmp_cfg.SVR_Channel, tmp_cfg.PLC_Channel, tmp_cfg.RTU_Channel = svr_c, plc_c, rtu_c
|
||||
tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = crd_c, pkt_c
|
||||
net_pane.set_value(2)
|
||||
net_pane.set_value(4)
|
||||
chan_err.hide(true)
|
||||
else chan_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,y=1,text="Please set the connection timeouts below."}
|
||||
TextBox{parent=net_c_4,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=8,width=11,text="PLC Timeout"}
|
||||
local plc_timeout = NumberField{parent=net_c_2,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=8,width=11,text="PLC Timeout"}
|
||||
local plc_timeout = NumberField{parent=net_c_4,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=9,width=19,text="RTU Gateway Timeout"}
|
||||
local rtu_timeout = NumberField{parent=net_c_2,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=9,width=19,text="RTU Gateway Timeout"}
|
||||
local rtu_timeout = NumberField{parent=net_c_4,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=10,width=19,text="Coordinator Timeout"}
|
||||
local crd_timeout = NumberField{parent=net_c_2,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=10,width=19,text="Coordinator Timeout"}
|
||||
local crd_timeout = NumberField{parent=net_c_4,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=11,width=14,text="Pocket Timeout"}
|
||||
local pkt_timeout = NumberField{parent=net_c_2,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_4,y=11,width=14,text="Pocket Timeout"}
|
||||
self.pkt_timeout = NumberField{parent=net_c_4,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
|
||||
TextBox{parent=net_c_2,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_4,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local ct_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(pkt_timeout.get_value())
|
||||
local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(self.pkt_timeout.get_value())
|
||||
|
||||
if not tmp_cfg.PocketEnabled then pkt_cto = tmp_cfg.PKT_Timeout or 5 end
|
||||
|
||||
if plc_cto ~= nil and rtu_cto ~= nil and crd_cto ~= nil and pkt_cto ~= nil then
|
||||
tmp_cfg.PLC_Timeout, tmp_cfg.RTU_Timeout, tmp_cfg.CRD_Timeout, tmp_cfg.PKT_Timeout = plc_cto, rtu_cto, crd_cto, pkt_cto
|
||||
net_pane.set_value(3)
|
||||
|
||||
if tmp_cfg.WirelessModem then
|
||||
net_pane.set_value(5)
|
||||
else
|
||||
tmp_cfg.TrustedRange = 0
|
||||
tmp_cfg.AuthKey = ""
|
||||
main_pane.set_value(4)
|
||||
end
|
||||
|
||||
ct_err.hide(true)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_5,y=1,text="Please set the wireless trusted range below."}
|
||||
TextBox{parent=net_c_5,y=3,height=3,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_5,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
local range = NumberField{parent=net_c_5,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local tr_err = TextBox{parent=net_c_5,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_tr()
|
||||
local range_val = tonumber(range.get_value())
|
||||
if range_val ~= nil then
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
net_pane.set_value(4)
|
||||
net_pane.set_value(6)
|
||||
tr_err.hide(true)
|
||||
else tr_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_5,y=14,text="\x1b Back",callback=function()net_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_5,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_6,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_6,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
|
||||
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_6,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
|
||||
local key, _ = TextField{parent=net_c_6,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
|
||||
|
||||
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
local hide_key = Checkbox{parent=net_c_6,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
local key_err = TextBox{parent=net_c_6,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
@@ -181,8 +306,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_6,y=14,text="\x1b Back",callback=function()net_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_6,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -190,17 +315,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
TextBox{parent=log_cfg,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
|
||||
TextBox{parent=log_c_1,y=1,text="Please configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
TextBox{parent=log_c_1,y=3,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=log_c_1,y=7,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
local en_dbg = Checkbox{parent=log_c_1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
@@ -217,7 +342,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -229,17 +354,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
local clr_pane = MultiPane{parent=clr_cfg,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
TextBox{parent=clr_cfg,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=clr_c_1,y=1,height=2,text="Here you can select the color theme for the front panel."}
|
||||
TextBox{parent=clr_c_1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_1,y=7,text="Front Panel Theme"}
|
||||
local fp_theme = RadioButton{parent=clr_c_1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
TextBox{parent=clr_c_2,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
@@ -268,8 +393,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
end
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
TextBox{parent=clr_c_2,y=7,width=10,text="Color Mode"}
|
||||
local c_mode = RadioButton{parent=clr_c_2,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
|
||||
|
||||
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
@@ -310,7 +435,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
@@ -322,12 +447,12 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
clr_pane.set_value(1)
|
||||
end
|
||||
|
||||
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_3,y=1,text="Settings saved!"}
|
||||
PushButton{parent=clr_c_3,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,x=1,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
TextBox{parent=clr_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=clr_c_4,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
PushButton{parent=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
@@ -339,11 +464,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
local sum_pane = MultiPane{parent=summary,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
TextBox{parent=summary,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local setting_list = ListBox{parent=sum_c_1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_settings()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
@@ -374,15 +499,22 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
|
||||
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
|
||||
try_set(tool_ctl.tank_mode, ini_cfg.FacilityTankMode)
|
||||
try_set(wireless, ini_cfg.WirelessModem)
|
||||
try_set(wired, ini_cfg.WiredModem ~= false)
|
||||
try_set(plc_listen, ini_cfg.PLC_Listen)
|
||||
try_set(rtu_listen, ini_cfg.RTU_Listen)
|
||||
try_set(crd_listen, ini_cfg.CRD_Listen)
|
||||
try_set(pkt_en, ini_cfg.PocketEnabled)
|
||||
try_set(self.pkt_test, ini_cfg.PocketTest)
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(plc_chan, ini_cfg.PLC_Channel)
|
||||
try_set(rtu_chan, ini_cfg.RTU_Channel)
|
||||
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(self.pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(plc_timeout, ini_cfg.PLC_Timeout)
|
||||
try_set(rtu_timeout, ini_cfg.RTU_Timeout)
|
||||
try_set(crd_timeout, ini_cfg.CRD_Timeout)
|
||||
try_set(pkt_timeout, ini_cfg.PKT_Timeout)
|
||||
try_set(self.pkt_timeout, ini_cfg.PKT_Timeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
@@ -406,6 +538,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
try_set(tool_ctl.aux_cool_elems[i].enable, ini_cfg.AuxiliaryCoolant[i])
|
||||
end
|
||||
|
||||
for i = 1, #ini_cfg.TankFluidTypes do
|
||||
if tool_ctl.tank_fluid_opts[i] then
|
||||
if (ini_cfg.TankFluidTypes[i] > 0) then
|
||||
tool_ctl.tank_fluid_opts[i].enable()
|
||||
tool_ctl.tank_fluid_opts[i].set_value(ini_cfg.TankFluidTypes[i])
|
||||
else
|
||||
tool_ctl.tank_fluid_opts[i].disable()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
@@ -421,11 +564,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
|
||||
TextBox{parent=sum_c_2,y=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
@@ -435,21 +578,21 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
TextBox{parent=sum_c_3,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/supervisor/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
TextBox{parent=sum_c_4,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
@@ -458,18 +601,51 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
|
||||
local i_err = Div{parent=import_err,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=import_err,x=1,y=2,text=" Import Error",fg_bg=cpair(colors.black,colors.red)}
|
||||
TextBox{parent=i_err,x=1,y=1,text="There is a problem with your config.lua file:"}
|
||||
TextBox{parent=import_err,y=2,text=" Import Error",fg_bg=cpair(colors.black,colors.red)}
|
||||
TextBox{parent=i_err,y=1,text="There is a problem with your config.lua file:"}
|
||||
|
||||
local import_err_msg = TextBox{parent=i_err,x=1,y=3,height=6,text=""}
|
||||
local import_err_msg = TextBox{parent=i_err,y=3,height=6,text=""}
|
||||
|
||||
PushButton{parent=i_err,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=i_err,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=i_err,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Tool Functions
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function self.show_auth_key()
|
||||
self.show_key_btn.disable()
|
||||
self.auth_key_textbox.set_value(self.auth_key_value)
|
||||
end
|
||||
|
||||
-- update the network interface configuration options
|
||||
function self.update_net_cfg()
|
||||
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
|
||||
plc_listen.enable()
|
||||
rtu_listen.enable()
|
||||
crd_listen.enable()
|
||||
else
|
||||
plc_listen.disable()
|
||||
rtu_listen.disable()
|
||||
crd_listen.disable()
|
||||
end
|
||||
|
||||
if tmp_cfg.WirelessModem then
|
||||
pkt_en.enable()
|
||||
self.pkt_test.enable()
|
||||
self.pkt_chan.enable()
|
||||
self.pkt_timeout.enable()
|
||||
else
|
||||
pkt_en.set_value(false)
|
||||
self.pkt_test.set_value(false)
|
||||
pkt_en.disable()
|
||||
self.pkt_test.disable()
|
||||
self.pkt_chan.disable()
|
||||
self.pkt_timeout.disable()
|
||||
end
|
||||
end
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("supervisor.config")
|
||||
@@ -524,6 +700,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
|
||||
tmp_cfg.FacilityTankList, tmp_cfg.FacilityTankConns = facility.generate_tank_list_and_conns(tmp_cfg.FacilityTankMode, tmp_cfg.FacilityTankDefs)
|
||||
|
||||
for i = 1, tmp_cfg.UnitCount do tmp_cfg.AuxiliaryCoolant[i] = false end
|
||||
for i = 1, tmp_cfg.FacilityTankList do tmp_cfg.TankFluidTypes[i] = types.COOLANT_TYPE.WATER end
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
|
||||
tmp_cfg.RTU_Channel = config.RTU_CHANNEL
|
||||
@@ -547,12 +726,6 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
self.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function self.show_auth_key()
|
||||
self.show_key_btn.disable()
|
||||
self.auth_key_textbox.set_value(self.auth_key_value)
|
||||
end
|
||||
|
||||
-- generate the summary list
|
||||
---@param cfg svr_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
@@ -675,6 +848,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
end
|
||||
|
||||
if val == "" then val = "no auxiliary coolant" end
|
||||
elseif f[1] == "PLC_Listen" or f[1] == "RTU_Listen" or f[1] == "CRD_Listen" then
|
||||
if raw == LISTEN_MODE.WIRELESS then val = "Wireless Only"
|
||||
elseif raw == LISTEN_MODE.WIRED then val = "Wired Only"
|
||||
elseif raw == LISTEN_MODE.ALL then val = "Wireless and Wired" end
|
||||
end
|
||||
|
||||
if not skip then
|
||||
@@ -693,7 +870,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
textbox = TextBox{parent=line,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
@@ -703,6 +880,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
|
||||
end
|
||||
end
|
||||
|
||||
-- generate the list of available/assigned wired modems
|
||||
function tool_ctl.gen_modem_list()
|
||||
modem_list.remove_all()
|
||||
|
||||
local enable = wired.get_value()
|
||||
|
||||
local function select(iface)
|
||||
tmp_cfg.WiredModem = iface
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
local modems = ppm.get_wired_modem_list()
|
||||
local missing = { tmp = true, ini = true }
|
||||
|
||||
for iface, _ in pairs(modems) do
|
||||
if ini_cfg.WiredModem == iface then missing.ini = false end
|
||||
if tmp_cfg.WiredModem == iface then missing.tmp = false end
|
||||
end
|
||||
|
||||
if missing.tmp and tmp_cfg.WiredModem then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
|
||||
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
|
||||
end
|
||||
|
||||
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
|
||||
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
|
||||
-- list wired modems
|
||||
for iface, _ in pairs(modems) do
|
||||
local line = Div{parent=modem_list,y=1,height=1}
|
||||
local used = tmp_cfg.WiredModem == iface
|
||||
|
||||
TextBox{parent=line,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
|
||||
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=line,x=15,y=1,text=iface}
|
||||
|
||||
if used or not enable then select_btn.disable() end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local facility = require("supervisor.config.facility")
|
||||
@@ -31,7 +33,8 @@ local CENTER = core.ALIGN.CENTER
|
||||
local changes = {
|
||||
{ "v1.2.12", { "Added front panel UI theme", "Added color accessibility modes" } },
|
||||
{ "v1.3.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
|
||||
{ "v1.6.0", { "Added sodium emergency coolant option" } }
|
||||
{ "v1.6.0", { "Added sodium emergency coolant option" } },
|
||||
{ "v1.8.0", { "Added support for wired communications modems", "Added option for allowing Pocket connections", "Added option for allowing Pocket test commands" } }
|
||||
}
|
||||
|
||||
---@class svr_configurator
|
||||
@@ -67,13 +70,16 @@ local tool_ctl = {
|
||||
num_units = nil, ---@type NumberField
|
||||
en_fac_tanks = nil, ---@type Checkbox
|
||||
tank_mode = nil, ---@type RadioButton
|
||||
tank_fluid_opts = {}, ---@type Radio2D[]
|
||||
|
||||
gen_summary = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[]
|
||||
tank_elems = {}, ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
|
||||
aux_cool_elems = {} ---@type { line: Div, enable: Checkbox }[]
|
||||
aux_cool_elems = {}, ---@type { line: Div, enable: Checkbox }[]
|
||||
|
||||
gen_modem_list = function () end
|
||||
}
|
||||
|
||||
---@class svr_config
|
||||
@@ -87,6 +93,13 @@ local tmp_cfg = {
|
||||
TankFluidTypes = {}, ---@type integer[] which type of fluid each tank in the tank list should be containing
|
||||
AuxiliaryCoolant = {}, ---@type boolean[] if a unit has auxiliary coolant
|
||||
ExtChargeIdling = false,
|
||||
WirelessModem = true,
|
||||
WiredModem = false, ---@type string|false
|
||||
PLC_Listen = 1, ---@type LISTEN_MODE
|
||||
RTU_Listen = 1, ---@type LISTEN_MODE
|
||||
CRD_Listen = 1, ---@type LISTEN_MODE
|
||||
PocketEnabled = true,
|
||||
PocketTest = true,
|
||||
SVR_Channel = nil, ---@type integer
|
||||
PLC_Channel = nil, ---@type integer
|
||||
RTU_Channel = nil, ---@type integer
|
||||
@@ -121,6 +134,13 @@ local fields = {
|
||||
{ "TankFluidTypes", "Tank Fluid Types", {} },
|
||||
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
|
||||
{ "ExtChargeIdling", "Extended Charge Idling", false },
|
||||
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
|
||||
{ "WiredModem", "Wired Comms Modem", false },
|
||||
{ "PLC_Listen", "PLC Listen Mode", types.LISTEN_MODE.WIRELESS },
|
||||
{ "RTU_Listen", "RTU Gateway Listen Mode", types.LISTEN_MODE.WIRELESS },
|
||||
{ "CRD_Listen", "Coordinator Listen Mode", types.LISTEN_MODE.WIRELESS },
|
||||
{ "PocketEnabled", "Pocket Connectivity", true },
|
||||
{ "PocketTest", "Pocket Testing Features", true },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "PLC_Channel", "PLC Channel", 16241 },
|
||||
{ "RTU_Channel", "RTU Channel", 16242 },
|
||||
@@ -131,7 +151,7 @@ local fields = {
|
||||
{ "CRD_Timeout", "CRD Connection Timeout", 5 },
|
||||
{ "PKT_Timeout", "PKT Connection Timeout", 5 },
|
||||
{ "TrustedRange", "Trusted Range", 0 },
|
||||
{ "AuthKey", "Facility Auth Key" , ""},
|
||||
{ "AuthKey", "Facility Auth Key" , "" },
|
||||
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||
{ "LogPath", "Log Path", "/log.txt" },
|
||||
{ "LogDebug", "Log Debug Messages", false },
|
||||
@@ -166,18 +186,18 @@ local function config_view(display)
|
||||
|
||||
TextBox{parent=display,y=1,text="Supervisor Configurator",alignment=CENTER,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
local root_pane_div = Div{parent=display,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local fac_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
local import_err = Div{parent=root_pane_div,x=1,y=1}
|
||||
local main_page = Div{parent=root_pane_div,y=1}
|
||||
local fac_cfg = Div{parent=root_pane_div,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,y=1}
|
||||
local clr_cfg = Div{parent=root_pane_div,y=1}
|
||||
local summary = Div{parent=root_pane_div,y=1}
|
||||
local changelog = Div{parent=root_pane_div,y=1}
|
||||
local import_err = Div{parent=root_pane_div,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,fac_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,import_err}}
|
||||
local main_pane = MultiPane{parent=root_pane_div,y=1,panes={main_page,fac_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,import_err}}
|
||||
|
||||
--#region Main Page
|
||||
|
||||
@@ -251,20 +271,20 @@ local function config_view(display)
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
TextBox{parent=changelog,y=2,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
local c_log = ListBox{parent=cl,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,text="- ",fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=cl,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
end
|
||||
@@ -286,11 +306,14 @@ function configurator.configure(ask_config)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
-- these need to be initialized as they are used before being set
|
||||
tmp_cfg.WiredModem = ini_cfg.WiredModem
|
||||
tmp_cfg.FacilityTankMode = ini_cfg.FacilityTankMode
|
||||
tmp_cfg.TankFluidTypes = { table.unpack(ini_cfg.TankFluidTypes) }
|
||||
|
||||
reset_term()
|
||||
|
||||
ppm.mount_all()
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
@@ -300,6 +323,8 @@ function configurator.configure(ask_config)
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
tool_ctl.gen_modem_list()
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3 = util.pull_event()
|
||||
|
||||
@@ -314,6 +339,14 @@ function configurator.configure(ask_config)
|
||||
if k_e then display.handle_key(k_e) end
|
||||
elseif event == "paste" then
|
||||
display.handle_paste(param1)
|
||||
elseif event == "peripheral_detach" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.handle_unmount(param1)
|
||||
tool_ctl.gen_modem_list()
|
||||
elseif event == "peripheral" then
|
||||
---@diagnostic disable-next-line: discard-returns
|
||||
ppm.mount(param1)
|
||||
tool_ctl.gen_modem_list()
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
|
||||
@@ -6,17 +6,27 @@ local const = require("scada-common.constants")
|
||||
local psil = require("scada-common.psil")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local pgi = require("supervisor.panel.pgi")
|
||||
local pgi = require("supervisor.panel.pgi")
|
||||
|
||||
local databus = {}
|
||||
|
||||
local _dbus = {
|
||||
wd_modem = true,
|
||||
wl_modem = true
|
||||
}
|
||||
|
||||
-- evaluate and publish system health status
|
||||
local function eval_status()
|
||||
databus.ps.publish("status", _dbus.wd_modem and _dbus.wl_modem)
|
||||
end
|
||||
|
||||
-- databus PSIL
|
||||
databus.ps = psil.create()
|
||||
|
||||
-- call to toggle heartbeat signal
|
||||
function databus.heartbeat() databus.ps.toggle("heartbeat") end
|
||||
|
||||
-- transmit firmware versions across the bus
|
||||
-- transmit firmware versions
|
||||
---@param sv_v string supervisor version
|
||||
---@param comms_v string comms version
|
||||
function databus.tx_versions(sv_v, comms_v)
|
||||
@@ -24,10 +34,22 @@ function databus.tx_versions(sv_v, comms_v)
|
||||
databus.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- transmit hardware status for modem connection state
|
||||
-- transmit hardware status for the wired comms modem
|
||||
---@param has_modem boolean
|
||||
function databus.tx_hw_modem(has_modem)
|
||||
databus.ps.publish("has_modem", has_modem)
|
||||
function databus.tx_hw_wd_modem(has_modem)
|
||||
databus.ps.publish("has_wd_modem", has_modem)
|
||||
|
||||
_dbus.wd_modem = has_modem
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit hardware status for the wireless comms modem
|
||||
---@param has_modem boolean
|
||||
function databus.tx_hw_wl_modem(has_modem)
|
||||
databus.ps.publish("has_wl_modem", has_modem)
|
||||
|
||||
_dbus.wl_modem = has_modem
|
||||
eval_status()
|
||||
end
|
||||
|
||||
-- transmit PLC firmware version and session connection state
|
||||
@@ -166,11 +188,4 @@ function databus.tx_pdg_rtt(session_id, rtt)
|
||||
end
|
||||
end
|
||||
|
||||
-- link a function to receive data from the bus
|
||||
---@param field string field name
|
||||
---@param func function function to link
|
||||
function databus.rx_field(field, func)
|
||||
databus.ps.subscribe(field, func)
|
||||
end
|
||||
|
||||
return databus
|
||||
|
||||
@@ -94,9 +94,13 @@ function facility.new(config)
|
||||
mode_set = PROCESS.MAX_BURN, ---@type PROCESS
|
||||
start_fail = START_STATUS.OK, ---@type START_STATUS
|
||||
max_burn_combined = 0.0, -- maximum burn rate to clamp at
|
||||
burn_target = 0.1, -- burn rate target for aggregate burn mode
|
||||
charge_setpoint = 0, -- FE charge target setpoint
|
||||
gen_rate_setpoint = 0, -- FE/t charge rate target setpoint
|
||||
sp = {
|
||||
burn_target = 0.1, -- burn rate target for aggregate burn mode
|
||||
range_start = 10, -- start threshold for range control
|
||||
range_stop = 90, -- stop threshold for range control
|
||||
charge_setpoint = 0, -- FE charge target setpoint
|
||||
gen_rate_setpoint = 0 -- FE/t charge rate target setpoint
|
||||
},
|
||||
group_map = {}, ---@type AUTO_GROUP[] units -> group IDs
|
||||
prio_defs = { {}, {}, {}, {} }, ---@type reactor_unit[][] priority definitions (each level is a table of units)
|
||||
at_max_burn = false,
|
||||
@@ -116,6 +120,7 @@ function facility.new(config)
|
||||
initial_ramp = true,
|
||||
waiting_on_ramp = false, -- waiting on auto ramping
|
||||
waiting_on_stable = false, -- waiting on gen rate stabilization
|
||||
range_control_en = false,
|
||||
accumulator = 0.0,
|
||||
saturated = false,
|
||||
last_update = 0,
|
||||
@@ -135,6 +140,7 @@ function facility.new(config)
|
||||
test_alarm_states = {}, ---@type { [ALARM]: boolean }
|
||||
-- statistics
|
||||
im_stat_init = false,
|
||||
imtx_percent = 0.0,
|
||||
avg_charge = util.mov_avg(3), -- 3 seconds
|
||||
avg_inflow = util.mov_avg(6), -- 3 seconds
|
||||
avg_outflow = util.mov_avg(6), -- 3 seconds
|
||||
@@ -207,21 +213,31 @@ function facility.new(config)
|
||||
|
||||
-- only allow changes if not running
|
||||
if self.mode == PROCESS.INACTIVE then
|
||||
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then
|
||||
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.RANGE_CONTROL) then
|
||||
self.mode_set = auto_cfg.mode
|
||||
end
|
||||
|
||||
ready = self.mode_set > 0
|
||||
|
||||
if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
|
||||
self.burn_target = auto_cfg.burn_target
|
||||
end
|
||||
self.sp.burn_target = auto_cfg.burn_target
|
||||
elseif self.mode_set == PROCESS.BURN_RATE then ready = false end
|
||||
|
||||
if (type(auto_cfg.range_start) == "number") and (auto_cfg.range_start >= 0) and (auto_cfg.range_start < 100) then
|
||||
self.sp.range_start = auto_cfg.range_start
|
||||
elseif self.mode_set == PROCESS.RANGE_CONTROL then ready = false end
|
||||
|
||||
if (type(auto_cfg.range_stop) == "number") and (auto_cfg.range_stop <= 100) and (auto_cfg.range_stop > auto_cfg.range_start) then
|
||||
self.sp.range_stop = auto_cfg.range_stop
|
||||
elseif self.mode_set == PROCESS.RANGE_CONTROL then ready = false end
|
||||
|
||||
if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then
|
||||
self.charge_setpoint = auto_cfg.charge_target * CHARGE_SCALER
|
||||
end
|
||||
self.sp.charge_setpoint = auto_cfg.charge_target * CHARGE_SCALER
|
||||
elseif self.mode_set == PROCESS.CHARGE then ready = false end
|
||||
|
||||
if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then
|
||||
self.gen_rate_setpoint = auto_cfg.gen_target * GEN_SCALER
|
||||
end
|
||||
self.sp.gen_rate_setpoint = auto_cfg.gen_target * GEN_SCALER
|
||||
elseif self.mode_set == PROCESS.GEN_RATE then ready = false end
|
||||
|
||||
if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then
|
||||
for i = 1, config.UnitCount do
|
||||
@@ -233,14 +249,6 @@ function facility.new(config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ready = self.mode_set > 0
|
||||
|
||||
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
|
||||
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
|
||||
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
|
||||
ready = false
|
||||
end
|
||||
end
|
||||
|
||||
return ready, limits
|
||||
@@ -455,9 +463,11 @@ function facility.new(config)
|
||||
return {
|
||||
ready,
|
||||
self.mode_set,
|
||||
self.burn_target,
|
||||
self.charge_setpoint / CHARGE_SCALER,
|
||||
self.gen_rate_setpoint / GEN_SCALER,
|
||||
self.sp.burn_target,
|
||||
self.sp.range_start,
|
||||
self.sp.range_stop,
|
||||
self.sp.charge_setpoint / CHARGE_SCALER,
|
||||
self.sp.gen_rate_setpoint / GEN_SCALER,
|
||||
limits
|
||||
}
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user