Compare commits
106 Commits
v1.8.18-be
...
v1.8.21-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e19418701 | ||
|
|
fb56634fc4 | ||
|
|
48fa715aaa | ||
|
|
753f062bfc | ||
|
|
356657c9c0 | ||
|
|
a4452ebbd2 | ||
|
|
35134822a9 | ||
|
|
f56d68d972 | ||
|
|
06933b2fb7 | ||
|
|
a1494b4afd | ||
|
|
2933b24318 | ||
|
|
402d8607b6 | ||
|
|
2e978db859 | ||
|
|
a7b3a2a0b8 | ||
|
|
6ff096fd31 | ||
|
|
13bb6cb026 | ||
|
|
5b311fcfbc | ||
|
|
d6a9f9c5f3 | ||
|
|
8ffbbb5ac9 | ||
|
|
bf10b3241e | ||
|
|
ab11ff03b5 | ||
|
|
66fae0695c | ||
|
|
dbd79cbc4f | ||
|
|
b5b67b425a | ||
|
|
f8bd79a234 | ||
|
|
07c3b3ec63 | ||
|
|
d7ea68ed3a | ||
|
|
db94ac7ff5 | ||
|
|
ee922a3aed | ||
|
|
75c77cc5b5 | ||
|
|
7683293c5e | ||
|
|
672a9c8dd1 | ||
|
|
3c10e28d03 | ||
|
|
035a26cc07 | ||
|
|
097edc5bf9 | ||
|
|
8a0d05c94b | ||
|
|
fbbd7e1ccd | ||
|
|
0f40c1d7f2 | ||
|
|
c299dce8ef | ||
|
|
11e9c11cf7 | ||
|
|
61ff055d60 | ||
|
|
f4be6519e8 | ||
|
|
705494bb7e | ||
|
|
610fb12bb3 | ||
|
|
07406ca5fc | ||
|
|
6b20445446 | ||
|
|
f93db02793 | ||
|
|
fe1b916b1f | ||
|
|
ebeeecc5ab | ||
|
|
dbabcd13b0 | ||
|
|
acc8e1c058 | ||
|
|
5a38acf2a7 | ||
|
|
b3be2d4bfc | ||
|
|
0ab2d57b66 | ||
|
|
183af8a5ca | ||
|
|
6f63092d4b | ||
|
|
a087eda0ee | ||
|
|
a1b6ff4bcc | ||
|
|
8c6b264f6b | ||
|
|
8a5c468606 | ||
|
|
12f187f596 | ||
|
|
01a1c374ab | ||
|
|
465875b287 | ||
|
|
45d4b4e653 | ||
|
|
fc7441b2f6 | ||
|
|
6917697290 | ||
|
|
c323967b6a | ||
|
|
4775639245 | ||
|
|
072613959c | ||
|
|
f259f85a99 | ||
|
|
e076e327d8 | ||
|
|
f34747372f | ||
|
|
affe2d6c6d | ||
|
|
5597ea2097 | ||
|
|
0f4a8b6dfc | ||
|
|
ab97f8935d | ||
|
|
b0342654e7 | ||
|
|
bee96ed12e | ||
|
|
50bd59781e | ||
|
|
196e0b1daf | ||
|
|
f725eb0eef | ||
|
|
bcc55628cf | ||
|
|
9bffd6feee | ||
|
|
1500004481 | ||
|
|
2904621e81 | ||
|
|
08eee198c8 | ||
|
|
e750ffe69d | ||
|
|
de6d8a89ca | ||
|
|
f00751edeb | ||
|
|
d58a6a3369 | ||
|
|
340c6689a9 | ||
|
|
7cc088ca95 | ||
|
|
01f6b1e190 | ||
|
|
3ffc79b181 | ||
|
|
8e4bb583a8 | ||
|
|
ec107929bc | ||
|
|
3406d12681 | ||
|
|
03bbf8a891 | ||
|
|
b61867be3c | ||
|
|
1358d95269 | ||
|
|
fd06730e46 | ||
|
|
fb5f3b9474 | ||
|
|
3afc1e6cfa | ||
|
|
715765d442 | ||
|
|
3762e9dced | ||
|
|
022d1f9f49 |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3.5.1
|
||||
uses: actions/checkout@v4
|
||||
- name: Luacheck
|
||||
uses: lunarmodules/luacheck@v1.1.0
|
||||
with:
|
||||
|
||||
16
.github/workflows/manifest.yml
vendored
16
.github/workflows/manifest.yml
vendored
@@ -29,13 +29,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v3.1.3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
# Generate manifest + shields files for main branch
|
||||
- name: Checkout main
|
||||
id: checkout-main
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'main'
|
||||
clean: false
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
run: python imgen.py shields
|
||||
run: python build/imgen.py shields
|
||||
- name: Save main's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/main
|
||||
@@ -54,7 +56,7 @@ jobs:
|
||||
- name: Checkout devel
|
||||
id: checkout-devel
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'devel'
|
||||
clean: false
|
||||
@@ -69,11 +71,11 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
id: upload-artifacts
|
||||
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload manifest JSON
|
||||
path: 'deploy/'
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -45,6 +45,7 @@ v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v
|
||||
You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get sqUN6VUb ccmsi.lua`
|
||||
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
84
ccmsi.lua
84
ccmsi.lua
@@ -15,15 +15,63 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local function println(message) print(tostring(message)) end
|
||||
local function print(message) term.write(tostring(message)) end
|
||||
|
||||
local CCMSI_VERSION = "v1.16"
|
||||
local CCMSI_VERSION = "v1.17"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local _is_pkt_env = pocket -- luacheck: ignore pocket
|
||||
|
||||
local function println(msg) print(tostring(msg)) end
|
||||
|
||||
-- stripped down & modified copy of log.dmesg
|
||||
local function print(msg)
|
||||
msg = tostring(msg)
|
||||
|
||||
local cur_x, cur_y = term.getCursorPos()
|
||||
local out_w, out_h = term.getSize()
|
||||
|
||||
-- jump to next line if needed
|
||||
if cur_x == out_w then
|
||||
cur_x = 1
|
||||
if cur_y == out_h then
|
||||
term.scroll(1)
|
||||
term.setCursorPos(1, cur_y)
|
||||
else
|
||||
term.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- wrap
|
||||
local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
|
||||
while remaining do
|
||||
local line = string.sub(msg, s_start, s_end)
|
||||
|
||||
if line == "" then
|
||||
remaining = false
|
||||
else
|
||||
lines[ln] = line
|
||||
s_start = s_end + 1
|
||||
s_end = s_end + out_w
|
||||
ln = ln + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- print
|
||||
for i = 1, #lines do
|
||||
cur_x, cur_y = term.getCursorPos()
|
||||
if i > 1 and cur_x > 1 then
|
||||
if cur_y == out_h then
|
||||
term.scroll(1)
|
||||
term.setCursorPos(1, cur_y)
|
||||
else term.setCursorPos(1, cur_y + 1) end
|
||||
end
|
||||
term.write(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
local opts = { ... }
|
||||
local mode, app, target
|
||||
local install_manifest = manifest_path.."main/install_manifest.json"
|
||||
@@ -219,10 +267,27 @@ end
|
||||
|
||||
-- get and validate command line options
|
||||
|
||||
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --")
|
||||
if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
|
||||
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
|
||||
|
||||
if #opts == 0 or opts[1] == "help" then
|
||||
println("usage: ccmsi <mode> <app> <branch>")
|
||||
if _is_pkt_env then
|
||||
yellow();println("<mode>");lgray()
|
||||
println(" check - check latest")
|
||||
println(" install - fresh install")
|
||||
println(" update - update app")
|
||||
println(" uninstall - remove app")
|
||||
yellow();println("<app>");lgray()
|
||||
println(" reactor-plc")
|
||||
println(" rtu")
|
||||
println(" supervisor")
|
||||
println(" coordinator")
|
||||
println(" pocket")
|
||||
println(" installer (update only)")
|
||||
yellow();println("<branch>");lgray();
|
||||
println(" main (default) | devel");white()
|
||||
else
|
||||
println("<mode>")
|
||||
lgray()
|
||||
println(" check - check latest versions available")
|
||||
@@ -241,6 +306,7 @@ if #opts == 0 or opts[1] == "help" then
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | devel");white()
|
||||
end
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||
@@ -286,20 +352,22 @@ if mode == "check" then
|
||||
-- list all versions
|
||||
for key, value in pairs(manifest.versions) do
|
||||
term.setTextColor(colors.purple)
|
||||
print(string.format("%-14s", "["..key.."]"))
|
||||
local tag = string.format("%-14s", "["..key.."]")
|
||||
if not _is_pkt_env then print(tag) end
|
||||
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
||||
if _is_pkt_env then println(tag) end
|
||||
blue();print(local_manifest.versions[key])
|
||||
if value ~= local_manifest.versions[key] then
|
||||
white();print(" (")
|
||||
cyan();print(value);white();println(" available)")
|
||||
else green();println(" (up to date)") end
|
||||
else
|
||||
elseif not _is_pkt_env then
|
||||
lgray();print("not installed");white();print(" (latest ")
|
||||
cyan();print(value);white();println(")")
|
||||
end
|
||||
end
|
||||
|
||||
if manifest.versions.installer ~= local_manifest.versions.installer then
|
||||
if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env then
|
||||
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
|
||||
end
|
||||
elseif mode == "install" or mode == "update" then
|
||||
|
||||
@@ -9,9 +9,9 @@ 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 themes = require("graphics.themes")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
@@ -38,7 +38,6 @@ local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
@@ -46,7 +45,8 @@ local RIGHT = core.ALIGN.RIGHT
|
||||
local changes = {
|
||||
{ "v1.2.4", { "Added temperature scale options" } },
|
||||
{ "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.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
|
||||
{ "v1.5.1", { "Added energy scale options" } }
|
||||
}
|
||||
|
||||
---@class crd_configurator
|
||||
@@ -70,7 +70,7 @@ local tool_ctl = {
|
||||
nic = nil, ---@type nic
|
||||
net_listen = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = 0,
|
||||
sv_seq_num = util.time_ms() * 10,
|
||||
sv_cool_conf = nil, ---@type table list of boiler & turbine counts
|
||||
show_sv_cfg = nil, ---@type function
|
||||
|
||||
@@ -119,6 +119,7 @@ local tmp_cfg = {
|
||||
SpeakerVolume = 1.0,
|
||||
Time24Hour = true,
|
||||
TempScale = 1,
|
||||
EnergyScale = 1,
|
||||
DisableFlowView = false,
|
||||
MainDisplay = nil, ---@type string
|
||||
FlowDisplay = nil, ---@type string
|
||||
@@ -151,7 +152,8 @@ local fields = {
|
||||
{ "UnitDisplays", "Unit Monitors", {} },
|
||||
{ "SpeakerVolume", "Speaker Volume", 1.0 },
|
||||
{ "Time24Hour", "Use 24-hour Time Format", true },
|
||||
{ "TempScale", "Temperature Scale", 1 },
|
||||
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
|
||||
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
|
||||
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
@@ -333,7 +335,7 @@ local function config_view(display)
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
elseif tool_ctl.start_fail > 0 then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
end
|
||||
|
||||
@@ -759,9 +761,13 @@ local function config_view(display)
|
||||
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
|
||||
local 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,x=24,y=8,text="Energy Scale"}
|
||||
local energy_scale = RadioButton{parent=crd_c_1,x=24,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}
|
||||
|
||||
local function submit_ui_opts()
|
||||
tmp_cfg.Time24Hour = clock_fmt.get_value() == 1
|
||||
tmp_cfg.TempScale = temp_scale.get_value()
|
||||
tmp_cfg.EnergyScale = energy_scale.get_value()
|
||||
main_pane.set_value(7)
|
||||
end
|
||||
|
||||
@@ -952,7 +958,10 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
for _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
|
||||
if settings.save("/coordinator.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
@@ -969,6 +978,8 @@ local function config_view(display)
|
||||
try_set(dis_flow_view, ini_cfg.DisableFlowView)
|
||||
try_set(s_vol, ini_cfg.SpeakerVolume)
|
||||
try_set(clock_fmt, util.trinary(ini_cfg.Time24Hour, 1, 2))
|
||||
try_set(temp_scale, ini_cfg.TempScale)
|
||||
try_set(energy_scale, ini_cfg.EnergyScale)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
@@ -1122,7 +1133,6 @@ local function config_view(display)
|
||||
tool_ctl.nic.open(tmp_cfg.CRD_Channel)
|
||||
|
||||
tool_ctl.sv_addr = comms.BROADCAST
|
||||
tool_ctl.sv_seq_num = 0
|
||||
tool_ctl.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD })
|
||||
@@ -1357,7 +1367,9 @@ local function config_view(display)
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "TempScale" then
|
||||
val = types.TEMP_SCALE_NAMES[raw]
|
||||
val = util.strval(types.TEMP_SCALE_NAMES[raw])
|
||||
elseif f[1] == "EnergyScale" then
|
||||
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
|
||||
elseif f[1] == "MainTheme" then
|
||||
val = util.strval(themes.ui_theme_name(raw))
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
@@ -1388,7 +1400,7 @@ local function config_view(display)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
@@ -38,6 +38,7 @@ function coordinator.load_config()
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
config.Time24Hour = settings.get("Time24Hour")
|
||||
config.TempScale = settings.get("TempScale")
|
||||
config.EnergyScale = settings.get("EnergyScale")
|
||||
|
||||
config.DisableFlowView = settings.get("DisableFlowView")
|
||||
config.MainDisplay = settings.get("MainDisplay")
|
||||
@@ -67,6 +68,8 @@ function coordinator.load_config()
|
||||
cfv.assert_type_bool(config.Time24Hour)
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
cfv.assert_type_int(config.EnergyScale)
|
||||
cfv.assert_range(config.EnergyScale, 1, 3)
|
||||
|
||||
cfv.assert_type_bool(config.DisableFlowView)
|
||||
cfv.assert_type_table(config.UnitDisplays)
|
||||
@@ -293,6 +296,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
self.sv_r_seq_num = nil
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
|
||||
end
|
||||
|
||||
@@ -383,7 +387,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param auto_cfg coord_auto_config configuration
|
||||
---@param auto_cfg sys_auto_config configuration
|
||||
function public.send_auto_start(auto_cfg)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
|
||||
@@ -517,7 +521,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
log.warning("sequence out-of-order: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return false
|
||||
elseif self.sv_linked and src_addr ~= self.sv_addr then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
|
||||
@@ -572,7 +576,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
process.fac_ack(cmd, ack)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
iocontrol.get_db().facility.stop_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
@@ -582,11 +586,13 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
process.fac_ack(cmd, ack)
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
process.waste_ack_handle(packet.data[2])
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
process.pu_fb_ack_handle(packet.data[2])
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
process.sps_lp_ack_handle(packet.data[2])
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
@@ -621,21 +627,15 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
unit.scram_ack(ack)
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
unit.start_ack(ack)
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
unit.reset_rps_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
unit.set_burn_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
unit.set_waste_ack(ack)
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_alarms_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
-- UI will be updated to display current group if changed successfully
|
||||
process.unit_ack(unit_id, cmd, ack)
|
||||
else
|
||||
log.debug(util.c("received unit command ack with unknown command ", cmd))
|
||||
log.debug(util.c("received unsupported unit command ack for command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug(util.c("received unit command ack with unknown unit ", unit_id))
|
||||
@@ -702,7 +702,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
if conf.num_units == config.UnitCount then
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public, config.TempScale)
|
||||
iocontrol.init(conf, public, config.TempScale, config.EnergyScale)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
|
||||
@@ -14,6 +14,9 @@ local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local PROCESS = types.PROCESS
|
||||
|
||||
local ENERGY_SCALE = types.ENERGY_SCALE
|
||||
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
|
||||
local TEMP_SCALE = types.TEMP_SCALE
|
||||
local TEMP_UNITS = types.TEMP_SCALE_UNITS
|
||||
|
||||
@@ -50,8 +53,10 @@ end
|
||||
---@param conf facility_conf configuration
|
||||
---@param comms coord_comms comms reference
|
||||
---@param temp_scale TEMP_SCALE temperature unit
|
||||
function iocontrol.init(conf, comms, temp_scale)
|
||||
io.temp_label = TEMP_UNITS[temp_scale]
|
||||
---@param energy_scale ENERGY_SCALE energy unit
|
||||
function iocontrol.init(conf, comms, temp_scale, energy_scale)
|
||||
io.temp_label = TEMP_UNITS[temp_scale]
|
||||
io.energy_label = ENERGY_UNITS[energy_scale]
|
||||
|
||||
-- temperature unit label and conversion function (from Kelvin)
|
||||
if temp_scale == TEMP_SCALE.CELSIUS then
|
||||
@@ -65,6 +70,18 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
io.temp_convert = function (t) return t end
|
||||
end
|
||||
|
||||
-- energy unit label and conversion function (from Joules unless otherwise specified)
|
||||
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
|
||||
io.energy_convert = util.joules_to_fe_rf
|
||||
io.energy_convert_from_fe = function (t) return t end
|
||||
io.energy_convert_to_fe = function (t) return t end
|
||||
else
|
||||
io.energy_label = "J"
|
||||
io.energy_convert = function (t) return t end
|
||||
io.energy_convert_from_fe = util.fe_rf_to_joules
|
||||
io.energy_convert_to_fe = util.joules_to_fe_rf
|
||||
end
|
||||
|
||||
-- facility data structure
|
||||
---@class ioctl_facility
|
||||
io.facility = {
|
||||
@@ -72,6 +89,7 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
num_units = conf.num_units,
|
||||
tank_mode = conf.cooling.fac_tank_mode,
|
||||
tank_defs = conf.cooling.fac_tank_defs,
|
||||
tank_list = conf.cooling.fac_tank_list,
|
||||
all_sys_ok = false,
|
||||
rtu_count = 0,
|
||||
|
||||
@@ -100,8 +118,6 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
save_cfg_ack = __generic_ack,
|
||||
start_ack = __generic_ack,
|
||||
stop_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
|
||||
alarm_tones = { false, false, false, false, false, false, false, false },
|
||||
|
||||
@@ -126,92 +142,6 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
table.insert(io.facility.sps_ps_tbl, psil.create())
|
||||
table.insert(io.facility.sps_data_tbl, {})
|
||||
|
||||
-- determine tank information
|
||||
if io.facility.tank_mode == 0 then
|
||||
io.facility.tank_defs = {}
|
||||
-- on facility tank mode 0, setup tank defs to match unit tank option
|
||||
for i = 1, conf.num_units do
|
||||
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
|
||||
end
|
||||
|
||||
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
|
||||
else
|
||||
-- decode the layout of tanks from the connections definitions
|
||||
local tank_mode = io.facility.tank_mode
|
||||
local tank_defs = io.facility.tank_defs
|
||||
local tank_list = { table.unpack(tank_defs) }
|
||||
|
||||
local function calc_fdef(start_idx, end_idx)
|
||||
local first = 4
|
||||
for i = start_idx, end_idx do
|
||||
if io.facility.tank_defs[i] == 2 then
|
||||
if i < first then first = i end
|
||||
end
|
||||
end
|
||||
return first
|
||||
end
|
||||
|
||||
if tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef = calc_fdef(1, #tank_defs)
|
||||
for i = 1, #tank_defs do
|
||||
if i > first_fdef and tank_defs[i] == 2 then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
|
||||
for i = 1, #tank_defs do
|
||||
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 3 then
|
||||
-- (3) 2 total facility tanks (A A B B)
|
||||
for _, a in pairs({ 1, 3 }) do
|
||||
local b = a + 1
|
||||
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
|
||||
tank_list[b] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef = calc_fdef(2, #tank_defs)
|
||||
for i = 1, #tank_defs do
|
||||
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
|
||||
for i = 1, #tank_defs do
|
||||
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
|
||||
for i = 1, #tank_defs do
|
||||
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
elseif tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef = calc_fdef(3, #tank_defs)
|
||||
for i = 1, #tank_defs do
|
||||
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
|
||||
tank_list[i] = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
io.facility.tank_list = tank_list
|
||||
end
|
||||
|
||||
-- create facility tank tables
|
||||
for i = 1, #io.facility.tank_list do
|
||||
if io.facility.tank_list[i] == 2 then
|
||||
@@ -252,24 +182,17 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
turbine_flow_stable = false,
|
||||
|
||||
-- auto control group
|
||||
a_group = 0,
|
||||
a_group = types.AUTO_GROUP.MANUAL,
|
||||
|
||||
start = function () process.start(i) end,
|
||||
scram = function () process.scram(i) end,
|
||||
reset_rps = function () process.reset_rps(i) end,
|
||||
ack_alarms = function () process.ack_all_alarms(i) end,
|
||||
start = function () io.process.start(i) end,
|
||||
scram = function () io.process.scram(i) end,
|
||||
reset_rps = function () io.process.reset_rps(i) end,
|
||||
ack_alarms = function () io.process.ack_all_alarms(i) end,
|
||||
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
|
||||
set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode
|
||||
|
||||
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual
|
||||
|
||||
start_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
reset_rps_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
set_burn_ack = __generic_ack,
|
||||
set_waste_ack = __generic_ack,
|
||||
|
||||
alarm_callbacks = {
|
||||
c_breach = { ack = function () ack(1) end, reset = function () reset(1) end },
|
||||
radiation = { ack = function () ack(2) end, reset = function () reset(2) end },
|
||||
@@ -349,6 +272,9 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
|
||||
-- pass IO control here since it can't be require'd due to a require loop
|
||||
process.init(io, comms)
|
||||
|
||||
-- coordinator's process handle
|
||||
io.process = process.create_handle()
|
||||
end
|
||||
|
||||
--#region Front Panel PSIL
|
||||
@@ -643,11 +569,10 @@ function iocontrol.update_facility_status(status)
|
||||
local group_map = ctl_status[14]
|
||||
|
||||
if (type(group_map) == "table") and (#group_map == fac.num_units) then
|
||||
local names = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
|
||||
for i = 1, #group_map do
|
||||
io.units[i].a_group = group_map[i]
|
||||
io.units[i].unit_ps.publish("auto_group_id", group_map[i])
|
||||
io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
|
||||
io.units[i].unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[group_map[i] + 1])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -692,7 +617,7 @@ function iocontrol.update_facility_status(status)
|
||||
ps.publish("is_discharging", out_f > in_f)
|
||||
|
||||
if data and data.build then
|
||||
local cap = util.joules_to_fe(data.build.transfer_cap)
|
||||
local cap = util.joules_to_fe_rf(data.build.transfer_cap)
|
||||
ps.publish("at_max_io", in_f >= cap or out_f >= cap)
|
||||
end
|
||||
else
|
||||
|
||||
@@ -7,21 +7,23 @@ local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local F_CMD = comms.FAC_COMMAND
|
||||
local U_CMD = comms.UNIT_COMMAND
|
||||
|
||||
local PROCESS = types.PROCESS
|
||||
local PRODUCT = types.WASTE_PRODUCT
|
||||
|
||||
local REQUEST_TIMEOUT_MS = 10000
|
||||
|
||||
---@class process_controller
|
||||
local process = {}
|
||||
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class coord_control_states
|
||||
local pctl = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class sys_control_states
|
||||
control_states = {
|
||||
---@class coord_auto_config
|
||||
---@class sys_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
@@ -34,28 +36,52 @@ local self = {
|
||||
},
|
||||
waste_modes = {},
|
||||
priority_groups = {}
|
||||
},
|
||||
commands = {
|
||||
unit = {}, ---@type process_command_state[][]
|
||||
fac = {} ---@type process_command_state[]
|
||||
}
|
||||
}
|
||||
|
||||
--------------------------
|
||||
-- UNIT COMMAND CONTROL --
|
||||
--------------------------
|
||||
---@class process_command_state
|
||||
---@field active boolean if this command is live
|
||||
---@field timeout integer expiration time of this command request
|
||||
---@field requestors table list of callbacks from the requestors
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- save config
|
||||
settings.set("ControlStates", pctl.control_states)
|
||||
local saved = settings.save("/coordinator.settings")
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return saved
|
||||
end
|
||||
|
||||
-- initialize the process controller
|
||||
---@param iocontrol ioctl iocontrl system
|
||||
---@param coord_comms coord_comms coordinator communications
|
||||
function process.init(iocontrol, coord_comms)
|
||||
self.io = iocontrol
|
||||
self.comms = coord_comms
|
||||
pctl.io = iocontrol
|
||||
pctl.comms = coord_comms
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
-- create command handling objects
|
||||
for _, v in pairs(F_CMD) do pctl.commands.fac[v] = { active = false, timeout = 0, requestors = {} } end
|
||||
for i = 1, pctl.io.facility.num_units do
|
||||
pctl.commands.unit[i] = {}
|
||||
for _, v in pairs(U_CMD) do pctl.commands.unit[i][v] = { active = false, timeout = 0, requestors = {} } end
|
||||
end
|
||||
|
||||
for i = 1, self.io.facility.num_units do
|
||||
local ctl_proc = pctl.control_states.process
|
||||
|
||||
for i = 1, pctl.io.facility.num_units do
|
||||
ctl_proc.limits[i] = 0.1
|
||||
end
|
||||
|
||||
local ctrl_states = settings.get("ControlStates", {})
|
||||
local config = ctrl_states.process ---@type coord_auto_config
|
||||
local config = ctrl_states.process ---@type sys_auto_config
|
||||
|
||||
-- facility auto control configuration
|
||||
if type(config) == "table" then
|
||||
@@ -68,33 +94,33 @@ function process.init(iocontrol, coord_comms)
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
ctl_proc.sps_low_power = config.sps_low_power
|
||||
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.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, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
|
||||
local unit = pctl.io.units[id] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
|
||||
-- notify supervisor of auto waste config
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power)
|
||||
end
|
||||
|
||||
-- unit waste states
|
||||
local waste_modes = ctrl_states.waste_modes ---@type table|nil
|
||||
if type(waste_modes) == "table" then
|
||||
for id, mode in pairs(waste_modes) do
|
||||
self.control_states.waste_modes[id] = mode
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
pctl.control_states.waste_modes[id] = mode
|
||||
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded unit waste mode settings")
|
||||
@@ -104,54 +130,208 @@ function process.init(iocontrol, coord_comms)
|
||||
local prio_groups = ctrl_states.priority_groups ---@type table|nil
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
self.control_states.priority_groups[id] = group
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||
pctl.control_states.priority_groups[id] = group
|
||||
pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded priority groups settings")
|
||||
end
|
||||
end
|
||||
|
||||
-- facility SCRAM command
|
||||
function process.fac_scram()
|
||||
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
-- create a handle to process control for usage of commands that get acknowledgements
|
||||
function process.create_handle()
|
||||
---@class process_handle
|
||||
local handle = {}
|
||||
|
||||
-- add this handle to the requestors and activate the command if inactive
|
||||
---@param cmd process_command_state
|
||||
---@param ack function
|
||||
local function request(cmd, ack)
|
||||
local new = not cmd.active
|
||||
|
||||
if new then
|
||||
cmd.active = true
|
||||
cmd.timeout = util.time_ms() + REQUEST_TIMEOUT_MS
|
||||
end
|
||||
|
||||
table.insert(cmd.requestors, ack)
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local function u_request(u_id, cmd_id, ack) return request(pctl.commands.unit[u_id][cmd_id], ack) end
|
||||
local function f_request(cmd_id, ack) return request(pctl.commands.fac[cmd_id], ack) end
|
||||
|
||||
--#region Facility Commands
|
||||
|
||||
-- facility SCRAM command
|
||||
function handle.fac_scram()
|
||||
if f_request(F_CMD.SCRAM_ALL, handle.fac_ack.on_scram) then
|
||||
pctl.comms.send_fac_command(F_CMD.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
end
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function handle.fac_ack_alarms()
|
||||
if f_request(F_CMD.ACK_ALL_ALARMS, handle.fac_ack.on_ack_alarms) then
|
||||
pctl.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
end
|
||||
end
|
||||
|
||||
handle.fac_ack = {}
|
||||
|
||||
-- luacheck: no unused args
|
||||
|
||||
-- facility SCRAM ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function handle.fac_ack.on_scram(success) end
|
||||
|
||||
-- facility acknowledge all alarms ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function handle.fac_ack.on_ack_alarms(success) end
|
||||
|
||||
-- luacheck: unused args
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Unit Commands
|
||||
|
||||
-- start a reactor
|
||||
---@param id integer unit ID
|
||||
function handle.start(id)
|
||||
if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then
|
||||
pctl.io.units[id].control_state = true
|
||||
pctl.comms.send_unit_command(U_CMD.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
end
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function handle.scram(id)
|
||||
if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then
|
||||
pctl.io.units[id].control_state = false
|
||||
pctl.comms.send_unit_command(U_CMD.SCRAM, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
|
||||
end
|
||||
end
|
||||
|
||||
-- reset reactor protection system
|
||||
---@param id integer unit ID
|
||||
function handle.reset_rps(id)
|
||||
if u_request(id, U_CMD.RESET_RPS, handle.unit_ack[id].on_rps_reset) then
|
||||
pctl.comms.send_unit_command(U_CMD.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
end
|
||||
end
|
||||
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function handle.ack_all_alarms(id)
|
||||
if u_request(id, U_CMD.ACK_ALL_ALARMS, handle.unit_ack[id].on_ack_alarms) then
|
||||
pctl.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
end
|
||||
|
||||
-- unit command acknowledgement callbacks, indexed by unit ID
|
||||
---@type process_unit_ack[]
|
||||
handle.unit_ack = {}
|
||||
|
||||
for u = 1, pctl.io.facility.num_units do
|
||||
handle.unit_ack[u] = {}
|
||||
|
||||
---@class process_unit_ack
|
||||
local u_ack = handle.unit_ack[u]
|
||||
|
||||
-- luacheck: no unused args
|
||||
|
||||
-- unit start ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_start(success) end
|
||||
|
||||
-- unit SCRAM ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_scram(success) end
|
||||
|
||||
-- unit RPS reset ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_rps_reset(success) end
|
||||
|
||||
-- unit acknowledge all alarms ack, override to implement
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function u_ack.on_ack_alarms(success) end
|
||||
|
||||
-- luacheck: unused args
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return handle
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function process.fac_ack_alarms()
|
||||
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
-- clear outstanding process commands that have timed out
|
||||
function process.clear_timed_out()
|
||||
local now = util.time_ms()
|
||||
local objs = { pctl.commands.fac, table.unpack(pctl.commands.unit) }
|
||||
|
||||
for _, obj in pairs(objs) do
|
||||
-- cancel expired requests
|
||||
for _, cmd in pairs(obj) do
|
||||
if cmd.active and now > cmd.timeout then
|
||||
cmd.active = false
|
||||
cmd.requestors = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- start reactor
|
||||
---@param id integer unit ID
|
||||
function process.start(id)
|
||||
self.io.units[id].control_state = true
|
||||
self.comms.send_unit_command(UNIT_COMMAND.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
-- handle a command acknowledgement
|
||||
---@param cmd_state process_command_state
|
||||
---@param success boolean if the command was successful
|
||||
local function cmd_ack(cmd_state, success)
|
||||
if cmd_state.active then
|
||||
cmd_state.active = false
|
||||
|
||||
-- call all acknowledge callback functions
|
||||
for i = 1, #cmd_state.requestors do
|
||||
cmd_state.requestors[i](success)
|
||||
end
|
||||
|
||||
cmd_state.requestors = {}
|
||||
end
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function process.scram(id)
|
||||
self.io.units[id].control_state = false
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
|
||||
-- handle a facility command acknowledgement
|
||||
---@param command FAC_COMMAND command
|
||||
---@param success boolean if the command was successful
|
||||
function process.fac_ack(command, success)
|
||||
cmd_ack(pctl.commands.fac[command], success)
|
||||
end
|
||||
|
||||
-- reset reactor protection system
|
||||
---@param id integer unit ID
|
||||
function process.reset_rps(id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
-- handle a unit command acknowledgement
|
||||
---@param unit integer unit ID
|
||||
---@param command UNIT_COMMAND command
|
||||
---@param success boolean if the command was successful
|
||||
function process.unit_ack(unit, command, success)
|
||||
cmd_ack(pctl.commands.unit[unit][command], success)
|
||||
end
|
||||
|
||||
--#region One-Way Commands (no acknowledgements)
|
||||
|
||||
-- set burn rate
|
||||
---@param id integer unit ID
|
||||
---@param rate number burn rate
|
||||
function process.set_rate(id, rate)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
|
||||
pctl.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
|
||||
end
|
||||
|
||||
@@ -160,31 +340,24 @@ end
|
||||
---@param mode integer waste mode
|
||||
function process.set_unit_waste(id, mode)
|
||||
-- publish so that if it fails then it gets reset
|
||||
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
pctl.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||
|
||||
self.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", self.control_states)
|
||||
pctl.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", pctl.control_states)
|
||||
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function process.ack_all_alarms(id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
|
||||
-- acknowledge an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.ack_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
|
||||
pctl.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
|
||||
end
|
||||
|
||||
@@ -192,7 +365,7 @@ end
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.reset_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
|
||||
pctl.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
|
||||
end
|
||||
|
||||
@@ -200,78 +373,68 @@ end
|
||||
---@param unit_id integer unit ID
|
||||
---@param group_id integer|0 group ID or 0 for independent
|
||||
function process.set_group(unit_id, group_id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
|
||||
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||
|
||||
self.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", self.control_states)
|
||||
pctl.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", pctl.control_states)
|
||||
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_group(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--------------------------
|
||||
-- AUTO PROCESS CONTROL --
|
||||
--------------------------
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- save config
|
||||
settings.set("ControlStates", self.control_states)
|
||||
local saved = settings.save("/coordinator.settings")
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return saved
|
||||
-- start automatic process control
|
||||
function process.start_auto()
|
||||
pctl.comms.send_auto_start(pctl.control_states.process)
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
end
|
||||
|
||||
-- stop automatic process control
|
||||
function process.stop_auto()
|
||||
self.comms.send_fac_command(FAC_COMMAND.STOP)
|
||||
pctl.comms.send_fac_command(F_CMD.STOP)
|
||||
log.debug("PROCESS: STOP AUTO CTL")
|
||||
end
|
||||
|
||||
-- start automatic process control
|
||||
function process.start_auto()
|
||||
self.comms.send_auto_start(self.control_states.process)
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
end
|
||||
|
||||
-- set automatic process control waste mode
|
||||
---@param product WASTE_PRODUCT waste product for auto control
|
||||
function process.set_process_waste(product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
|
||||
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.waste_product = product
|
||||
pctl.control_states.process.waste_product = product
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control plutonium fallback
|
||||
---@param enabled boolean whether to enable plutonium fallback
|
||||
function process.set_pu_fallback(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.pu_fallback = enabled
|
||||
pctl.control_states.process.pu_fallback = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control SPS usage at low power
|
||||
---@param enabled boolean whether to enable SPS usage at low power
|
||||
function process.set_sps_low_power(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled)
|
||||
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.sps_low_power = enabled
|
||||
pctl.control_states.process.sps_low_power = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
@@ -285,7 +448,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- update config table
|
||||
local ctl_proc = self.control_states.process
|
||||
local ctl_proc = pctl.control_states.process
|
||||
ctl_proc.mode = mode
|
||||
ctl_proc.burn_target = burn_target
|
||||
ctl_proc.charge_target = charge_target
|
||||
@@ -293,7 +456,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
ctl_proc.limits = limits
|
||||
|
||||
-- save config
|
||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||
pctl.io.facility.save_cfg_ack(_write_auto_config())
|
||||
end
|
||||
|
||||
-- handle a start command acknowledgement
|
||||
@@ -301,39 +464,46 @@ end
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
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]
|
||||
|
||||
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
||||
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
local unit = pctl.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
end
|
||||
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
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))
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
pctl.io.facility.start_ack(ack)
|
||||
end
|
||||
|
||||
-- record waste product state after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product state
|
||||
-- record waste product settting after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product settting
|
||||
function process.waste_ack_handle(response)
|
||||
self.control_states.process.waste_product = response
|
||||
self.io.facility.ps.publish("process_waste_product", response)
|
||||
pctl.control_states.process.waste_product = response
|
||||
pctl.io.facility.ps.publish("process_waste_product", response)
|
||||
end
|
||||
|
||||
-- record plutonium fallback state after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback state
|
||||
-- record plutonium fallback settting after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback settting
|
||||
function process.pu_fb_ack_handle(response)
|
||||
self.control_states.process.pu_fallback = response
|
||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||
pctl.control_states.process.pu_fallback = response
|
||||
pctl.io.facility.ps.publish("process_pu_fallback", response)
|
||||
end
|
||||
|
||||
-- record SPS low power settting after attempting to change it
|
||||
---@param response boolean supervisor SPS low power settting
|
||||
function process.sps_lp_ack_handle(response)
|
||||
pctl.control_states.process.sps_low_power = response
|
||||
pctl.io.facility.ps.publish("process_sps_low_power", response)
|
||||
end
|
||||
|
||||
return process
|
||||
|
||||
@@ -4,12 +4,15 @@ local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
@@ -37,7 +40,7 @@ local PERIODICS = {
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
local log_header = "pkt_session(" .. id .. "): "
|
||||
local log_tag = "pkt_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
-- connection properties
|
||||
@@ -46,6 +49,8 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
connected = true,
|
||||
conn_watchdog = util.new_watchdog(timeout),
|
||||
last_rtt = 0,
|
||||
-- process accessor handle
|
||||
proc_handle = process.create_handle(),
|
||||
-- periodic messages
|
||||
periodics = {
|
||||
last_update = 0,
|
||||
@@ -101,12 +106,24 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- link callback transmissions
|
||||
|
||||
self.proc_handle.fac_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
|
||||
self.proc_handle.fac_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
|
||||
|
||||
for u = 1, iocontrol.get_db().facility.num_units do
|
||||
self.proc_handle.unit_ack[u].on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
|
||||
self.proc_handle.unit_ack[u].on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
|
||||
self.proc_handle.unit_ack[u].on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
|
||||
self.proc_handle.unit_ack[u].on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param pkt mgmt_frame|crdn_frame
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num() + 1
|
||||
@@ -122,7 +139,68 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == CRDN_TYPE.API_GET_FAC then
|
||||
if pkt.type == CRDN_TYPE.FAC_CMD then
|
||||
if pkt.length >= 1 then
|
||||
local cmd = pkt.data[1]
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
log.info(log_tag .. "FAC SCRAM ALL")
|
||||
self.proc_handle.fac_scram()
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
log.info(log_tag .. "FAC ACK ALL ALARMS")
|
||||
self.proc_handle.fac_ack_alarms()
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
else
|
||||
log.debug(log_tag .. "CRDN facility command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN facility command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.UNIT_CMD then
|
||||
if pkt.length >= 2 then
|
||||
-- get command and unit id
|
||||
local cmd = pkt.data[1]
|
||||
local uid = pkt.data[2]
|
||||
|
||||
-- continue if valid unit id
|
||||
if util.is_int(uid) and uid > 0 and uid <= #db.units then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] SCRAM"))
|
||||
self.proc_handle.scram(uid)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] START"))
|
||||
self.proc_handle.start(uid)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
|
||||
self.proc_handle.reset_rps(uid)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
if pkt.length == 3 then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
|
||||
process.set_rate(uid, pkt.data[3])
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command burn rate missing option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
|
||||
self.proc_handle.ack_all_alarms(uid)
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALARM then
|
||||
elseif cmd == UNIT_COMMAND.RESET_ALARM then
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command invalid")
|
||||
end
|
||||
else
|
||||
log.debug(log_tag .. "CRDN unit command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_FAC then
|
||||
local fac = db.facility
|
||||
|
||||
local data = {
|
||||
@@ -146,6 +224,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
u.unit_id,
|
||||
u.connected,
|
||||
u.rtu_hw,
|
||||
u.a_group,
|
||||
u.alarms,
|
||||
u.annunciator,
|
||||
u.reactor_data,
|
||||
@@ -160,7 +239,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
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
|
||||
@@ -173,7 +252,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_tag .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
@@ -181,13 +260,17 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
|
||||
iocontrol.fp_pkt_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == MGMT_TYPE.ESTABLISH then
|
||||
-- something is wrong, kill the session
|
||||
_close()
|
||||
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -212,7 +295,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_header .. "session closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -243,14 +326,14 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
log.info(log_tag .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local COORDINATOR_VERSION = "v1.5.1"
|
||||
local COORDINATOR_VERSION = "v1.5.8"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ local util = require("scada-common.util")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
@@ -147,6 +148,9 @@ function threads.thread__main(smem)
|
||||
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))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -34,6 +36,8 @@ local function new_view(root, x, y, data, ps, id)
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
|
||||
@@ -48,24 +52,24 @@ local function new_view(root, x, y, data, ps, id)
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,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:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
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}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
|
||||
energy.register(ps, "energy", function (val) energy.update(db.energy_convert(val)) end)
|
||||
avg_chg.register(ps, "avg_charge", avg_chg.update)
|
||||
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
||||
input.register(ps, "last_input", function (val) input.update(db.energy_convert(val)) end)
|
||||
avg_in.register(ps, "avg_inflow", avg_in.update)
|
||||
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
||||
output.register(ps, "last_output", function (val) output.update(db.energy_convert(val)) end)
|
||||
avg_out.register(ps, "avg_outflow", avg_out.update)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
|
||||
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
|
||||
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
|
||||
@@ -56,16 +56,18 @@ local function new_view(root, x, y)
|
||||
local blk_brn = cpair(colors.black, colors.brown)
|
||||
local blk_pur = cpair(colors.black, colors.purple)
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
local db = iocontrol.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=process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
|
||||
local 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 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}
|
||||
|
||||
facility.scram_ack = scram.on_response
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
db.process.fac_ack.on_scram = scram.on_response
|
||||
db.process.fac_ack.on_ack_alarms = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}
|
||||
@@ -141,22 +143,22 @@ local function new_view(root, x, y)
|
||||
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
|
||||
local c_target = SpinboxNumeric{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="MFE",fg_bg=style.theme.label_fg}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
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}
|
||||
|
||||
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(db.energy_convert_from_fe(fe) / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t",fg_bg=style.theme.label_fg}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
|
||||
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
||||
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(db.energy_convert(j) / 1000)) end)
|
||||
|
||||
-----------------
|
||||
-- unit limits --
|
||||
@@ -262,7 +264,10 @@ local function new_view(root, x, y)
|
||||
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(), c_target.get_value(), g_target.get_value(), limits)
|
||||
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)
|
||||
end
|
||||
|
||||
-- start automatic control after saving process control settings
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
local util = require("scada-common.util")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
@@ -24,14 +24,16 @@ 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 turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
|
||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg}
|
||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",unit=db.energy_label,format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg}
|
||||
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(db.energy_convert(val)) end)
|
||||
flow_rate.register(ps, "steam_input_rate", flow_rate.update)
|
||||
|
||||
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
|
||||
@@ -29,6 +29,8 @@ local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
@@ -373,16 +375,16 @@ local function init(parent, id)
|
||||
local scram = HazardButton{parent=main,x=2,y=32,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg}
|
||||
local reset = HazardButton{parent=main,x=22,y=32,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg}
|
||||
|
||||
unit.start_ack = start.on_response
|
||||
unit.scram_ack = scram.on_response
|
||||
unit.reset_rps_ack = reset.on_response
|
||||
unit.ack_alarms_ack = ack_a.on_response
|
||||
db.process.unit_ack[id].on_start = start.on_response
|
||||
db.process.unit_ack[id].on_scram = scram.on_response
|
||||
db.process.unit_ack[id].on_rps_reset = reset.on_response
|
||||
db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
|
||||
|
||||
local function start_button_en_check()
|
||||
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
|
||||
local can_start = (not unit.reactor_data.mek_status.status) and
|
||||
(not unit.reactor_data.rps_tripped) and
|
||||
(unit.a_group == 0)
|
||||
(unit.a_group == AUTO_GROUP.MANUAL)
|
||||
if can_start then start.enable() else start.disable() end
|
||||
end
|
||||
end
|
||||
@@ -486,9 +488,7 @@ local function init(parent, id)
|
||||
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 ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
|
||||
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,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,callback=function()end,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)
|
||||
|
||||
@@ -523,10 +523,10 @@ local function init(parent, id)
|
||||
|
||||
-- enable/disable controls based on group assignment (start button is separate)
|
||||
burn_rate.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
|
||||
if gid == AUTO_GROUP.MANUAL then burn_rate.enable() else burn_rate.disable() end
|
||||
end)
|
||||
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
end)
|
||||
|
||||
-- can't change group if auto is engaged regardless of if this unit is part of auto control
|
||||
|
||||
@@ -132,8 +132,8 @@ local function init(panel, num_units)
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
|
||||
|
||||
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)
|
||||
|
||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.3.0"
|
||||
core.version = "2.3.4"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
@@ -123,15 +123,17 @@ end
|
||||
|
||||
-- Interactive Field Manager
|
||||
|
||||
---@param e graphics_base
|
||||
---@param max_len any
|
||||
---@param fg_bg any
|
||||
---@param dis_fg_bg any
|
||||
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
---@param e graphics_base element
|
||||
---@param max_len any max value length
|
||||
---@param fg_bg any enabled fg/bg
|
||||
---@param dis_fg_bg any disabled fg/bg
|
||||
---@param align_right any true to align content right while unfocused
|
||||
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
|
||||
local self = {
|
||||
frame_start = 1,
|
||||
visible_text = e.value,
|
||||
cursor_pos = string.len(e.value) + 1,
|
||||
align_offset = 0,
|
||||
selected_all = false
|
||||
}
|
||||
|
||||
@@ -186,7 +188,12 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
e.w_write(string.rep(" ", e.frame.w))
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local function _write()
|
||||
local function _write(align_r)
|
||||
if align_r and string.len(self.visible_text) <=e.frame.w then
|
||||
self.align_offset = (e.frame.w - string.len(self.visible_text))
|
||||
e.w_set_cur((e.frame.w - string.len(self.visible_text)) + 1, 1)
|
||||
end
|
||||
|
||||
if self.censor then
|
||||
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
|
||||
else
|
||||
@@ -226,15 +233,27 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
self.selected_all = false
|
||||
|
||||
-- write text without cursor
|
||||
_write()
|
||||
_write(align_right)
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
-- get an x value to pass to move_cursor taking into account right alignment offset present when unfocused
|
||||
---@param x integer
|
||||
function public.get_cursor_align_shift(x)
|
||||
return math.max(0, x - self.align_offset)
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
---@param x integer x position or 0 to jump to the end
|
||||
function public.move_cursor(x)
|
||||
self.selected_all = false
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
|
||||
if x <= 0 then
|
||||
self.cursor_pos = string.len(self.visible_text) + 1
|
||||
else
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
end
|
||||
|
||||
public.show()
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-- Generic Graphics Element
|
||||
--
|
||||
|
||||
-- local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -503,7 +504,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
|
||||
|
||||
if args.parent ~= nil then
|
||||
-- remove self from parent
|
||||
-- log.debug("removing " .. self.id .. " from parent")
|
||||
args.parent.__remove_child(self.id)
|
||||
else
|
||||
-- log.debug("no parent for " .. self.id .. " on delete attempt")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -570,6 +574,15 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
|
||||
---@return graphics_element
|
||||
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
|
||||
|
||||
-- get all children
|
||||
---@nodiscard
|
||||
---@return table children table of graphics_element objects
|
||||
function public.get_children()
|
||||
local list = {}
|
||||
for k, v in pairs(protected.children) do list[k] = v.get() end
|
||||
return list
|
||||
end
|
||||
|
||||
-- remove a child element
|
||||
---@param id element_id
|
||||
function public.remove(id)
|
||||
|
||||
@@ -10,6 +10,7 @@ local element = require("graphics.element")
|
||||
---@field accent color accent color for hazard border
|
||||
---@field dis_colors? cpair text color and border color when disabled
|
||||
---@field callback function function to call on touch
|
||||
---@field timeout? integer override for the default 1.5 second timeout, in seconds
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
@@ -28,6 +29,8 @@ local function hazard_button(args)
|
||||
args.height = 3
|
||||
args.width = string.len(args.text) + 4
|
||||
|
||||
local timeout = args.timeout or 1.5
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
@@ -149,8 +152,8 @@ local function hazard_button(args)
|
||||
tcd.abort(on_success)
|
||||
tcd.abort(on_failure)
|
||||
|
||||
-- 1.5 second timeout
|
||||
tcd.dispatch(1.5, on_timeout)
|
||||
-- operation timeout animation
|
||||
tcd.dispatch(timeout, on_timeout)
|
||||
|
||||
args.callback()
|
||||
end
|
||||
|
||||
@@ -17,6 +17,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
|
||||
---@field allow_decimal? boolean true to allow decimals
|
||||
---@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 parent graphics_element
|
||||
---@field id? string element id
|
||||
@@ -47,7 +48,7 @@ local function number_field(args)
|
||||
e.value = "" .. (args.default or 0)
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg)
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
@@ -55,10 +56,16 @@ local function number_field(args)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
if core.events.was_clicked(event.type) then
|
||||
local x = event.current.x
|
||||
|
||||
if not e.is_focused() then
|
||||
x = ifield.get_cursor_align_shift(x)
|
||||
end
|
||||
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP then
|
||||
ifield.move_cursor(event.current.x)
|
||||
ifield.move_cursor(x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
|
||||
@@ -6,6 +6,7 @@ local element = require("graphics.element")
|
||||
|
||||
---@class power_indicator_args
|
||||
---@field label string indicator label
|
||||
---@field unit string energy unit
|
||||
---@field format string power format override (lua string format)
|
||||
---@field rate boolean? whether to append /t to the end (power per tick)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
@@ -23,6 +24,8 @@ local element = require("graphics.element")
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.unit) == "string", "unit is a required field")
|
||||
element.assert(type(args.value) == "number", "value is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
@@ -40,7 +43,7 @@ local function power(args)
|
||||
function e.on_update(value)
|
||||
e.value = value
|
||||
|
||||
local data_str, unit = util.power_format(value, false, args.format)
|
||||
local data_str, unit = util.power_format(value, args.unit, false, args.format)
|
||||
|
||||
-- write data
|
||||
e.w_set_cur(data_start, 1)
|
||||
@@ -53,14 +56,13 @@ local function power(args)
|
||||
end
|
||||
|
||||
-- append per tick if rate is set
|
||||
-- add space to FE so we don't end up with FEE (after having kFE for example)
|
||||
if args.rate == true then
|
||||
unit = unit .. "/t"
|
||||
if unit == "FE/t" then unit = "FE/t " end
|
||||
else
|
||||
if unit == "FE" then unit = "FE " end
|
||||
end
|
||||
|
||||
-- add space to unit so we don't end up with something like FEE after having kFE
|
||||
unit = util.strminw(unit, 5)
|
||||
|
||||
e.w_write(" " .. unit)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- Scroll-able List Box Display Graphics Element
|
||||
|
||||
-- local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -152,6 +153,7 @@ local function listbox(args)
|
||||
next_y = next_y + item.h + item_pad
|
||||
item.e.reposition(1, item.y)
|
||||
item.e.show()
|
||||
-- log.debug("iterated " .. item.e.get_id())
|
||||
end
|
||||
|
||||
content_height = next_y
|
||||
@@ -210,6 +212,7 @@ local function listbox(args)
|
||||
---@param child graphics_element child element
|
||||
function e.on_added(id, child)
|
||||
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
||||
-- log.debug("added child " .. id .. " into slot " .. #list)
|
||||
update_positions()
|
||||
end
|
||||
|
||||
@@ -219,10 +222,12 @@ local function listbox(args)
|
||||
for idx, elem in ipairs(list) do
|
||||
if elem.id == id then
|
||||
table.remove(list, idx)
|
||||
-- log.debug("removed child " .. id .. " from slot " .. idx)
|
||||
update_positions()
|
||||
return
|
||||
end
|
||||
end
|
||||
-- log.debug("failed to remove child " .. id)
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
|
||||
@@ -57,6 +57,9 @@ local function textbox(args)
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
|
||||
-- trim leading/trailing whitespace
|
||||
lines[i] = util.trim(lines[i])
|
||||
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
|
||||
@@ -27,13 +27,13 @@ local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {
|
||||
{ "v0.9.2", { "Added temperature scale options" } }
|
||||
{ "v0.9.2", { "Added temperature scale options" } },
|
||||
{ "v0.11.3", { "Added energy scale options" } }
|
||||
}
|
||||
|
||||
---@class pkt_configurator
|
||||
@@ -76,6 +76,7 @@ local tool_ctl = {
|
||||
---@class pkt_config
|
||||
local tmp_cfg = {
|
||||
TempScale = 1,
|
||||
EnergyScale = 1,
|
||||
SVR_Channel = nil, ---@type integer
|
||||
CRD_Channel = nil, ---@type integer
|
||||
PKT_Channel = nil, ---@type integer
|
||||
@@ -94,7 +95,8 @@ local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "TempScale", "Temperature Scale", 1 },
|
||||
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
|
||||
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
@@ -175,13 +177,17 @@ local function config_view(display)
|
||||
|
||||
TextBox{parent=ui_cfg,x=1,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 use the options below to customize formats."}
|
||||
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."}
|
||||
|
||||
TextBox{parent=ui_c_1,x=1,y=5,text="Temperature Scale"}
|
||||
local temp_scale = RadioButton{parent=ui_c_1,x=1,y=6,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_1,x=1,y=4,text="Temperature Scale"}
|
||||
local temp_scale = RadioButton{parent=ui_c_1,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_1,x=1,y=10,text="Energy Scale"}
|
||||
local energy_scale = RadioButton{parent=ui_c_1,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}
|
||||
|
||||
local function submit_ui_opts()
|
||||
tmp_cfg.TempScale = temp_scale.get_value()
|
||||
tmp_cfg.EnergyScale = energy_scale.get_value()
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
|
||||
@@ -372,12 +378,17 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
for _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
|
||||
if settings.save("/pocket.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(temp_scale, ini_cfg.TempScale)
|
||||
try_set(energy_scale, ini_cfg.EnergyScale)
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||
@@ -504,7 +515,9 @@ local function config_view(display)
|
||||
elseif f[1] == "LogMode" then
|
||||
val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "TempScale" then
|
||||
val = types.TEMP_SCALE_NAMES[raw]
|
||||
val = util.strval(types.TEMP_SCALE_NAMES[raw])
|
||||
elseif f[1] == "EnergyScale" then
|
||||
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
|
||||
end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
@@ -522,7 +535,7 @@ local function config_view(display)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
-- I/O Control for Pocket Integration with Supervisor & Coordinator
|
||||
--
|
||||
|
||||
local const = require("scada-common.constants")
|
||||
-- local log = require("scada-common.log")
|
||||
local psil = require("scada-common.psil")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local const = require("scada-common.constants")
|
||||
local psil = require("scada-common.psil")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local process = require("pocket.process")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
local ENERGY_SCALE = types.ENERGY_SCALE
|
||||
local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
|
||||
local TEMP_SCALE = types.TEMP_SCALE
|
||||
local TEMP_UNITS = types.TEMP_SCALE_UNITS
|
||||
|
||||
@@ -35,10 +39,26 @@ local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
-- luacheck: no unused args
|
||||
|
||||
-- placeholder acknowledge function for type hinting
|
||||
---@param success boolean
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
local function __generic_ack(success) end
|
||||
|
||||
-- luacheck: unused args
|
||||
|
||||
local config = nil ---@type pkt_config
|
||||
local comms = nil ---@type pocket_comms
|
||||
|
||||
-- initialize facility-independent components of pocket iocontrol
|
||||
---@param comms pocket_comms
|
||||
---@param pkt_comms pocket_comms
|
||||
---@param nav pocket_nav
|
||||
function iocontrol.init_core(comms, nav)
|
||||
---@param cfg pkt_config
|
||||
function iocontrol.init_core(pkt_comms, nav, cfg)
|
||||
comms = pkt_comms
|
||||
config = cfg
|
||||
|
||||
io.nav = nav
|
||||
|
||||
---@class pocket_ioctl_diag
|
||||
@@ -86,10 +106,11 @@ function iocontrol.init_core(comms, nav)
|
||||
end
|
||||
|
||||
-- initialize facility-dependent components of pocket iocontrol
|
||||
---@param conf facility_conf configuration
|
||||
---@param temp_scale TEMP_SCALE temperature unit
|
||||
function iocontrol.init_fac(conf, temp_scale)
|
||||
---@param conf facility_conf facility configuration
|
||||
function iocontrol.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]
|
||||
|
||||
-- temperature unit label and conversion function (from Kelvin)
|
||||
if temp_scale == TEMP_SCALE.CELSIUS then
|
||||
@@ -103,6 +124,18 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
io.temp_convert = function (t) return t end
|
||||
end
|
||||
|
||||
-- energy unit label and conversion function (from Joules unless otherwise specified)
|
||||
if energy_scale == ENERGY_SCALE.FE or energy_scale == ENERGY_SCALE.RF then
|
||||
io.energy_convert = util.joules_to_fe_rf
|
||||
io.energy_convert_from_fe = function (t) return t end
|
||||
io.energy_convert_to_fe = function (t) return t end
|
||||
else
|
||||
io.energy_label = "J"
|
||||
io.energy_convert = function (t) return t end
|
||||
io.energy_convert_from_fe = util.fe_rf_to_joules
|
||||
io.energy_convert_to_fe = util.joules_to_fe_rf
|
||||
end
|
||||
|
||||
-- facility data structure
|
||||
---@class pioctl_facility
|
||||
io.facility = {
|
||||
@@ -133,6 +166,11 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
start_ack = __generic_ack,
|
||||
stop_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
|
||||
ps = psil.create(),
|
||||
|
||||
induction_ps_tbl = {},
|
||||
@@ -277,7 +315,18 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
turbine_flow_stable = false,
|
||||
|
||||
-- auto control group
|
||||
a_group = 0,
|
||||
a_group = types.AUTO_GROUP.MANUAL,
|
||||
|
||||
start = function () process.start(i) end,
|
||||
scram = function () process.scram(i) end,
|
||||
reset_rps = function () process.reset_rps(i) end,
|
||||
ack_alarms = function () process.ack_all_alarms(i) end,
|
||||
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
|
||||
|
||||
start_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
reset_rps_ack = __generic_ack,
|
||||
ack_alarms_ack = __generic_ack,
|
||||
|
||||
---@type alarms
|
||||
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
|
||||
@@ -325,12 +374,15 @@ function iocontrol.init_fac(conf, temp_scale)
|
||||
|
||||
table.insert(io.units, entry)
|
||||
end
|
||||
|
||||
-- pass IO control here since it can't be require'd due to a require loop
|
||||
process.init(io, comms)
|
||||
end
|
||||
|
||||
-- set network link state
|
||||
---@param state POCKET_LINK_STATE
|
||||
---@param sv_addr integer? supervisor address if linked
|
||||
---@param api_addr integer? coordinator address if linked
|
||||
---@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)
|
||||
io.ps.publish("link_state", state)
|
||||
|
||||
@@ -342,10 +394,25 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
io.ps.publish("crd_conn_quality", 0)
|
||||
end
|
||||
|
||||
if sv_addr then io.ps.publish("sv_addr", sv_addr) end
|
||||
if api_addr then io.ps.publish("api_addr", api_addr) end
|
||||
if sv_addr then
|
||||
io.ps.publish("sv_addr", util.c(sv_addr, ":", config.SVR_Channel))
|
||||
elseif sv_addr == false then
|
||||
io.ps.publish("sv_addr", "unknown (not linked)")
|
||||
end
|
||||
|
||||
if api_addr then
|
||||
io.ps.publish("api_addr", util.c(api_addr, ":", config.CRD_Channel))
|
||||
elseif api_addr == false then
|
||||
io.ps.publish("api_addr", "unknown (not linked)")
|
||||
end
|
||||
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
|
||||
|
||||
-- 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
|
||||
|
||||
-- determine supervisor connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_svr_tt(trip_time)
|
||||
@@ -422,11 +489,15 @@ function iocontrol.record_unit_data(data)
|
||||
|
||||
unit.connected = data[2]
|
||||
unit.rtu_hw = data[3]
|
||||
unit.alarms = data[4]
|
||||
unit.a_group = data[4]
|
||||
unit.alarms = data[5]
|
||||
|
||||
unit.unit_ps.publish("auto_group_id", unit.a_group)
|
||||
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
|
||||
|
||||
--#region Annunciator
|
||||
|
||||
unit.annunciator = data[5]
|
||||
unit.annunciator = data[6]
|
||||
|
||||
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
|
||||
|
||||
@@ -504,7 +575,7 @@ function iocontrol.record_unit_data(data)
|
||||
|
||||
--#region Reactor Data
|
||||
|
||||
unit.reactor_data = data[6]
|
||||
unit.reactor_data = data[7]
|
||||
|
||||
local control_status = 1
|
||||
local reactor_status = 1
|
||||
@@ -576,7 +647,7 @@ function iocontrol.record_unit_data(data)
|
||||
|
||||
--#region RTU Devices
|
||||
|
||||
unit.boiler_data_tbl = data[7]
|
||||
unit.boiler_data_tbl = data[8]
|
||||
|
||||
for id = 1, #unit.boiler_data_tbl do
|
||||
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
@@ -609,7 +680,7 @@ function iocontrol.record_unit_data(data)
|
||||
ps.publish("BoilerStateStatus", computed_status)
|
||||
end
|
||||
|
||||
unit.turbine_data_tbl = data[8]
|
||||
unit.turbine_data_tbl = data[9]
|
||||
|
||||
for id = 1, #unit.turbine_data_tbl do
|
||||
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||
@@ -644,16 +715,16 @@ function iocontrol.record_unit_data(data)
|
||||
ps.publish("TurbineStateStatus", computed_status)
|
||||
end
|
||||
|
||||
unit.tank_data_tbl = data[9]
|
||||
unit.tank_data_tbl = data[10]
|
||||
|
||||
unit.last_rate_change_ms = data[10]
|
||||
unit.turbine_flow_stable = data[11]
|
||||
unit.last_rate_change_ms = data[11]
|
||||
unit.turbine_flow_stable = data[12]
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Status Information Display
|
||||
|
||||
local ecam = {} -- aviation reference :) back to VATSIM I go...
|
||||
local ecam = {} -- aviation reference :)
|
||||
|
||||
-- local function red(text) return { text = text, color = colors.red } end
|
||||
local function white(text) return { text = text, color = colors.white } end
|
||||
|
||||
@@ -9,6 +9,8 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
|
||||
@@ -36,6 +38,7 @@ function pocket.load_config()
|
||||
if not settings.load("/pocket.settings") then return false end
|
||||
|
||||
config.TempScale = settings.get("TempScale")
|
||||
config.EnergyScale = settings.get("EnergyScale")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
@@ -52,6 +55,8 @@ function pocket.load_config()
|
||||
|
||||
cfv.assert_type_int(config.TempScale)
|
||||
cfv.assert_range(config.TempScale, 1, 4)
|
||||
cfv.assert_type_int(config.EnergyScale)
|
||||
cfv.assert_range(config.EnergyScale, 1, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
@@ -81,13 +86,14 @@ local APP_ID = {
|
||||
LOADER = 2,
|
||||
-- main app pages
|
||||
UNITS = 3,
|
||||
GUIDE = 4,
|
||||
ABOUT = 5,
|
||||
CONTROL = 4,
|
||||
GUIDE = 5,
|
||||
ABOUT = 6,
|
||||
-- diag app page
|
||||
ALARMS = 6,
|
||||
ALARMS = 7,
|
||||
-- other
|
||||
DUMMY = 7,
|
||||
NUM_APPS = 7
|
||||
DUMMY = 8,
|
||||
NUM_APPS = 8
|
||||
}
|
||||
|
||||
pocket.APP_ID = APP_ID
|
||||
@@ -436,11 +442,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
|
||||
-- attempt supervisor connection establishment
|
||||
local function _send_sv_establish()
|
||||
self.sv.r_seq_num = nil
|
||||
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
end
|
||||
|
||||
-- attempt coordinator API connection establishment
|
||||
local function _send_api_establish()
|
||||
self.api.r_seq_num = nil
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
|
||||
end
|
||||
|
||||
@@ -490,7 +498,11 @@ 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
|
||||
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
|
||||
end
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_sv_establish()
|
||||
@@ -499,7 +511,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
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)
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
|
||||
|
||||
if self.establish_delay_counter <= 0 then
|
||||
_send_api_establish()
|
||||
@@ -507,9 +519,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
else
|
||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||
end
|
||||
else
|
||||
-- linked, all good!
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -537,6 +546,21 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMAND command
|
||||
---@param option any? optional option options for the optional options (like waste mode)
|
||||
function public.send_fac_command(cmd, option)
|
||||
_send_api(CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send a unit command
|
||||
---@param cmd UNIT_COMMAND command
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_api(CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
@@ -577,7 +601,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0))
|
||||
if not ok then
|
||||
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d"
|
||||
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.scada_frame.length()))
|
||||
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.length))
|
||||
end
|
||||
return ok
|
||||
end
|
||||
@@ -606,7 +630,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
if self.api.r_seq_num == nil then
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.api.linked and (src_addr ~= self.api.addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
|
||||
@@ -622,12 +646,56 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.api.linked then
|
||||
if packet.type == CRDN_TYPE.API_GET_FAC then
|
||||
if packet.type == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.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
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
if packet.length == 3 then
|
||||
local cmd = packet.data[1]
|
||||
local unit_id = packet.data[2]
|
||||
local ack = packet.data[3] == true
|
||||
|
||||
local unit = iocontrol.get_db().units[unit_id] ---@type pioctl_unit
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
unit.scram_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
unit.start_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
unit.reset_rps_ack(ack)
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_alarms_ack(ack)
|
||||
else
|
||||
log.debug(util.c("received unsupported unit command ack for command ", cmd))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_FAC then
|
||||
if _check_length(packet, 11) then
|
||||
iocontrol.record_facility_data(packet.data)
|
||||
end
|
||||
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
|
||||
if _check_length(packet, 11) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
|
||||
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
|
||||
iocontrol.record_unit_data(packet.data)
|
||||
end
|
||||
else _fail_type(packet) end
|
||||
@@ -675,17 +743,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, config.TempScale)
|
||||
iocontrol.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("")
|
||||
|
||||
if self.sv.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
|
||||
iocontrol.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")
|
||||
@@ -693,24 +763,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
else
|
||||
log.debug("received coordinator establish allow without facility configuration")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator comms version mismatch")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
|
||||
if self.api.last_est_ack ~= est_ack then
|
||||
log.info("coordinator api version mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
||||
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")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
log.info("coordinator connection denied due to collision")
|
||||
iocontrol.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")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
|
||||
log.info("coordinator api version mismatch")
|
||||
iocontrol.report_crd_link_error("API version mismatch")
|
||||
else
|
||||
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
||||
iocontrol.report_crd_link_error("unknown reply")
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink
|
||||
self.api.addr = comms.BROADCAST
|
||||
self.api.linked = false
|
||||
end
|
||||
|
||||
self.api.last_est_ack = est_ack
|
||||
@@ -726,7 +801,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
if self.sv.r_seq_num == nil then
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.sv.linked and (src_addr ~= self.sv.addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
|
||||
@@ -822,25 +897,33 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
self.sv.linked = true
|
||||
self.sv.addr = src_addr
|
||||
|
||||
iocontrol.report_svr_link_error("")
|
||||
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
log.info("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.sv.last_est_ack ~= est_ack then
|
||||
log.info("supervisor comms version mismatch")
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
|
||||
end
|
||||
else
|
||||
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||
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")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
log.info("supervisor connection denied due to collision")
|
||||
iocontrol.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")
|
||||
else
|
||||
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||
iocontrol.report_svr_link_error("unknown reply")
|
||||
end
|
||||
end
|
||||
|
||||
-- unlink
|
||||
self.sv.addr = comms.BROADCAST
|
||||
self.sv.linked = false
|
||||
end
|
||||
|
||||
self.sv.last_est_ack = est_ack
|
||||
|
||||
94
pocket/process.lua
Normal file
94
pocket/process.lua
Normal file
@@ -0,0 +1,94 @@
|
||||
--
|
||||
-- Process Control Management
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
---@class pocket_process_controller
|
||||
local process = {}
|
||||
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil ---@type pocket_comms
|
||||
}
|
||||
|
||||
-- initialize the process controller
|
||||
---@param iocontrol pocket_ioctl iocontrl system
|
||||
---@param pocket_comms pocket_comms pocket communications
|
||||
function process.init(iocontrol, pocket_comms)
|
||||
self.io = iocontrol
|
||||
self.comms = pocket_comms
|
||||
end
|
||||
|
||||
-- facility SCRAM command
|
||||
function process.fac_scram()
|
||||
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function process.fac_ack_alarms()
|
||||
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
end
|
||||
|
||||
-- start reactor
|
||||
---@param id integer unit ID
|
||||
function process.start(id)
|
||||
self.io.units[id].control_state = true
|
||||
self.comms.send_unit_command(UNIT_COMMAND.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function process.scram(id)
|
||||
self.io.units[id].control_state = false
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
|
||||
end
|
||||
|
||||
-- reset reactor protection system
|
||||
---@param id integer unit ID
|
||||
function process.reset_rps(id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
end
|
||||
|
||||
-- set burn rate
|
||||
---@param id integer unit ID
|
||||
---@param rate number burn rate
|
||||
function process.set_rate(id, rate)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
|
||||
end
|
||||
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function process.ack_all_alarms(id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
|
||||
-- acknowledge an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.ack_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- reset an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.reset_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
|
||||
end
|
||||
|
||||
return process
|
||||
@@ -20,7 +20,7 @@ local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
local threads = require("pocket.threads")
|
||||
|
||||
local POCKET_VERSION = "v0.11.2-alpha"
|
||||
local POCKET_VERSION = "v0.12.1-alpha"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -152,7 +152,7 @@ local function main()
|
||||
log.debug("startup> comms init")
|
||||
|
||||
-- init I/O control
|
||||
iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav)
|
||||
iocontrol.init_core(smem_sys.pocket_comms, smem_sys.nav, config)
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
|
||||
233
pocket/ui/apps/control.lua
Normal file
233
pocket/ui/apps/control.lua
Normal file
@@ -0,0 +1,233 @@
|
||||
--
|
||||
-- Unit Control Page
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
local process = require("pocket.process")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local WaitingAnim = require("graphics.elements.animations.waiting")
|
||||
|
||||
local HazardButton = require("graphics.elements.controls.hazard_button")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IconIndicator = require("graphics.elements.indicators.icon")
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = pocket.APP_ID
|
||||
|
||||
local lu_col = style.label_unit_pair
|
||||
local text_fg = style.text_fg
|
||||
local mode_states = style.icon_states.mode_states
|
||||
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
-- new unit control page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local frame = Div{parent=root,x=1,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}
|
||||
|
||||
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}}
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
|
||||
|
||||
local btn_fg_bg = cpair(colors.green, colors.black)
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
|
||||
local page_div = nil ---@type nil|graphics_element
|
||||
|
||||
-- set sidebar to display unit-specific fields based on a specified unit
|
||||
local function set_sidebar()
|
||||
local list = {
|
||||
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
|
||||
{ label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end }
|
||||
}
|
||||
|
||||
for i = 1, db.facility.num_units do
|
||||
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
|
||||
end
|
||||
|
||||
app.set_sidebar(list)
|
||||
end
|
||||
|
||||
-- load the app (create the elements)
|
||||
local function load()
|
||||
page_div = Div{parent=main,y=2,width=main.get_width()}
|
||||
|
||||
local panes = {}
|
||||
|
||||
local active_unit = 1
|
||||
|
||||
-- create all page divs
|
||||
for _ = 1, db.facility.num_units + 1 do
|
||||
local div = Div{parent=page_div}
|
||||
table.insert(panes, div)
|
||||
end
|
||||
|
||||
-- previous unit
|
||||
local function prev(x)
|
||||
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
|
||||
app.switcher(active_unit)
|
||||
end
|
||||
|
||||
-- next unit
|
||||
local function next(x)
|
||||
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
|
||||
app.switcher(active_unit)
|
||||
end
|
||||
|
||||
for i = 1, db.facility.num_units do
|
||||
local u_pane = panes[i]
|
||||
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
|
||||
local unit = db.units[i] ---@type pioctl_unit
|
||||
local u_ps = unit.unit_ps
|
||||
|
||||
-- refresh data callback, every 500ms it will re-send the query
|
||||
local last_update = 0
|
||||
local function update()
|
||||
if util.time_ms() - last_update >= 500 then
|
||||
db.api.get_unit(i)
|
||||
last_update = util.time_ms()
|
||||
end
|
||||
end
|
||||
|
||||
local u_page = app.new_page(nil, i)
|
||||
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,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}
|
||||
|
||||
rate.register(u_ps, "act_burn_rate", rate.update)
|
||||
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
|
||||
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
|
||||
|
||||
u_div.line_break()
|
||||
|
||||
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.lightGray)}
|
||||
|
||||
local set_burn = function () unit.set_burn(burn_cmd.get_value()) end
|
||||
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=cpair(colors.gray,colors.black),callback=set_burn}
|
||||
|
||||
-- enable/disable controls based on group assignment (start button is separate)
|
||||
burn_cmd.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == AUTO_GROUP.MANUAL then burn_cmd.enable() else burn_cmd.disable() end
|
||||
end)
|
||||
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
|
||||
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
|
||||
end)
|
||||
|
||||
burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value)
|
||||
burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max)
|
||||
|
||||
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
|
||||
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg}
|
||||
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg}
|
||||
|
||||
unit.start_ack = start.on_response
|
||||
unit.ack_alarms_ack = ack_a.on_response
|
||||
unit.scram_ack = scram.on_response
|
||||
unit.reset_rps_ack = reset.on_response
|
||||
|
||||
local function start_button_en_check()
|
||||
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
|
||||
local can_start = (not unit.reactor_data.mek_status.status) and
|
||||
(not unit.reactor_data.rps_tripped) and
|
||||
(unit.a_group == AUTO_GROUP.MANUAL)
|
||||
if can_start then start.enable() else start.disable() end
|
||||
end
|
||||
end
|
||||
|
||||
start.register(u_ps, "status", start_button_en_check)
|
||||
start.register(u_ps, "rps_tripped", start_button_en_check)
|
||||
start.register(u_ps, "auto_group_id", start_button_en_check)
|
||||
start.register(u_ps, "AutoControl", start_button_en_check)
|
||||
|
||||
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- facility controls
|
||||
|
||||
local f_pane = panes[db.facility.num_units + 1]
|
||||
local f_div = Div{parent=f_pane,x=2,width=main.get_width()-2}
|
||||
|
||||
app.new_page(nil, db.facility.num_units + 1)
|
||||
|
||||
TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER}
|
||||
|
||||
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
|
||||
|
||||
db.facility.scram_ack = scram.on_response
|
||||
db.facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
set_sidebar()
|
||||
|
||||
-- done, show the app
|
||||
load_pane.set_value(2)
|
||||
end
|
||||
|
||||
-- delete the elements and switch back to the loading screen
|
||||
local function unload()
|
||||
if page_div then
|
||||
page_div.delete()
|
||||
page_div = nil
|
||||
end
|
||||
|
||||
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
|
||||
app.delete_pages()
|
||||
|
||||
-- show loading screen
|
||||
load_pane.set_value(1)
|
||||
end
|
||||
|
||||
app.set_load(load)
|
||||
app.set_unload(unload)
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -3,6 +3,7 @@
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
@@ -78,6 +79,7 @@ local function new_view(root)
|
||||
local uis_page = app.new_page(main_page, 4)
|
||||
local fps_page = app.new_page(main_page, 5)
|
||||
local gls_page = app.new_page(main_page, 6)
|
||||
local lnk_page = app.new_page(main_page, 7)
|
||||
|
||||
local home = Div{parent=page_div,x=2}
|
||||
local search = Div{parent=page_div,x=2}
|
||||
@@ -85,7 +87,8 @@ local function new_view(root)
|
||||
local uis = Div{parent=page_div,x=2,width=p_width}
|
||||
local fps = Div{parent=page_div,x=2,width=p_width}
|
||||
local gls = Div{parent=page_div,x=2,width=p_width}
|
||||
local panes = { home, search, use, uis, fps, gls }
|
||||
local lnk = Div{parent=page_div,x=2,width=p_width}
|
||||
local panes = { home, search, use, uis, fps, gls, lnk }
|
||||
|
||||
local doc_map = {}
|
||||
local search_db = {}
|
||||
@@ -100,6 +103,7 @@ local function new_view(root)
|
||||
PushButton{parent=home,text="Operator UIs >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
|
||||
PushButton{parent=home,text="Front Panels >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fps_page.nav_to}
|
||||
PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to}
|
||||
PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to}
|
||||
|
||||
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
|
||||
|
||||
@@ -113,43 +117,52 @@ local function new_view(root)
|
||||
|
||||
function func_ref.run_search()
|
||||
local query = string.lower(query_field.get_value())
|
||||
local s_results = { {}, {}, {} }
|
||||
local s_results = { {}, {}, {}, {} }
|
||||
|
||||
search_results.remove_all()
|
||||
|
||||
if string.len(query) < 3 then
|
||||
TextBox{parent=search_results,text="Search requires at least 3 characters."}
|
||||
if string.len(query) < 2 then
|
||||
TextBox{parent=search_results,text="Search requires at least 2 characters."}
|
||||
return
|
||||
end
|
||||
|
||||
local start = util.time_ms()
|
||||
|
||||
for _, entry in ipairs(search_db) do
|
||||
local s_start, _ = string.find(entry[1], query, 1, true)
|
||||
local s_start, s_end = string.find(entry[1], query, 1, true)
|
||||
|
||||
if s_start == nil then
|
||||
elseif s_start == 1 then
|
||||
-- best match, start of key
|
||||
table.insert(s_results[1], entry)
|
||||
if s_end == string.len(entry[1]) then
|
||||
-- best match: full match
|
||||
table.insert(s_results[1], entry)
|
||||
else
|
||||
-- very good match, start of key
|
||||
table.insert(s_results[2], entry)
|
||||
end
|
||||
elseif string.sub(query, s_start - 1, s_start) == " " then
|
||||
-- start of word, good match
|
||||
table.insert(s_results[2], entry)
|
||||
table.insert(s_results[3], entry)
|
||||
else
|
||||
-- basic match in content
|
||||
table.insert(s_results[3], entry)
|
||||
table.insert(s_results[4], entry)
|
||||
end
|
||||
end
|
||||
|
||||
local empty = true
|
||||
|
||||
for tier = 1, 3 do
|
||||
for tier = 1, 4 do
|
||||
for idx = 1, #s_results[tier] do
|
||||
local entry = s_results[tier][idx]
|
||||
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)}
|
||||
PushButton{parent=search_results,text=entry[2],alignment=ALIGN.LEFT,fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
|
||||
PushButton{parent=search_results,text=entry[2],fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
|
||||
|
||||
empty = false
|
||||
end
|
||||
end
|
||||
|
||||
log.debug("App.Guide: search for \"" .. query .. "\" completed in " .. (util.time_ms() - start) .. "ms")
|
||||
|
||||
if empty then
|
||||
TextBox{parent=search_results,text="No results found."}
|
||||
end
|
||||
@@ -188,7 +201,8 @@ local function new_view(root)
|
||||
local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
|
||||
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
|
||||
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
|
||||
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.unit.fac_section, 100)
|
||||
|
||||
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
|
||||
|
||||
PushButton{parent=annunc_div,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
|
||||
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
|
||||
@@ -199,21 +213,39 @@ local function new_view(root)
|
||||
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
|
||||
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
|
||||
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
|
||||
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
|
||||
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
|
||||
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 180)
|
||||
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
|
||||
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
|
||||
|
||||
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to}
|
||||
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to}
|
||||
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to}
|
||||
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to}
|
||||
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
|
||||
|
||||
TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
|
||||
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
|
||||
|
||||
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 130)
|
||||
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140)
|
||||
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 100)
|
||||
|
||||
PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
|
||||
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
|
||||
|
||||
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}
|
||||
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="GitHub",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada"}
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="Wiki",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada/wiki"}
|
||||
lnk.line_break()
|
||||
TextBox{parent=lnk,text="Discord",fg_bg=cpair(colors.lightGray,colors.black)}
|
||||
TextBox{parent=lnk,text="discord.gg/R9NSCkhcwt"}
|
||||
|
||||
-- setup multipane
|
||||
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
app.set_root_pane(u_pane)
|
||||
|
||||
@@ -63,25 +63,25 @@ local function create_pages(root)
|
||||
|
||||
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",fg_bg=label}
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel),alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel)}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Supervisor Address",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local sv = TextBox{parent=nt_div,x=2,text="",alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text="Supervisor Address",fg_bg=label}
|
||||
local sv = TextBox{parent=nt_div,x=2,text=""}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Coordinator Address",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local coord = TextBox{parent=nt_div,x=2,text="",alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text="Coordinator Address",fg_bg=label}
|
||||
local coord = TextBox{parent=nt_div,x=2,text=""}
|
||||
|
||||
sv.register(db.ps, "sv_addr", function (addr) sv.set_value(util.c(addr, ":", config.SVR_Channel)) end)
|
||||
coord.register(db.ps, "api_addr", function (addr) coord.set_value(util.c(addr, ":", config.CRD_Channel)) end)
|
||||
sv.register(db.ps, "sv_addr", sv.set_value)
|
||||
coord.register(db.ps, "api_addr", coord.set_value)
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Message Authentication",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=nt_div,x=2,text="Message Authentication",fg_bg=label}
|
||||
local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None")
|
||||
TextBox{parent=nt_div,x=2,text=auth,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text=auth}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -96,28 +96,28 @@ local function create_pages(root)
|
||||
|
||||
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
|
||||
|
||||
TextBox{parent=fw_list,x=2,text="Pocket Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Pocket Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Comms Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Comms Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="API Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.api_version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="API Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.api_version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Common Lib Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=util.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Common Lib Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=util.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Graphics Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=core.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Graphics Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=core.version}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Lockbox Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=lockbox.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Lockbox Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=lockbox.version}
|
||||
|
||||
--#endregion
|
||||
|
||||
@@ -129,12 +129,12 @@ local function create_pages(root)
|
||||
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Lua Version",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_VERSION,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=hw_div,x=2,text="Lua Version",fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_VERSION}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Environment",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=hw_div,x=2,text="Environment",fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_HOST,height=6}
|
||||
|
||||
--#endregion
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
-- Connection Waiting Spinner
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -23,16 +25,20 @@ local function init(parent, y, is_api)
|
||||
local root = Div{parent=parent,x=1,y=1}
|
||||
|
||||
-- bounding box div
|
||||
local box = Div{parent=root,x=1,y=y,height=6}
|
||||
local box = Div{parent=root,x=1,y=y,height=12}
|
||||
|
||||
local waiting_x = math.floor(parent.get_width() / 2) - 1
|
||||
|
||||
local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg)}
|
||||
|
||||
if is_api then
|
||||
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
|
||||
TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
msg.register(iocontrol.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,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
|
||||
msg.register(iocontrol.get_db().ps, "svr_link_msg", msg.set_value)
|
||||
end
|
||||
|
||||
return root
|
||||
|
||||
@@ -1,13 +1,65 @@
|
||||
local const = require("scada-common.constants")
|
||||
|
||||
local docs = {}
|
||||
|
||||
---@enum DOC_ITEM_TYPE
|
||||
local DOC_ITEM_TYPE = {
|
||||
SECTION = 1,
|
||||
SUBSECTION = 2,
|
||||
TEXT = 3,
|
||||
LIST = 4
|
||||
}
|
||||
|
||||
---@enum DOC_LIST_TYPE
|
||||
local DOC_LIST_TYPE = {
|
||||
BULLET = 1,
|
||||
NUMBERED = 2,
|
||||
INDICATOR = 3,
|
||||
LED = 4
|
||||
}
|
||||
|
||||
docs.DOC_ITEM_TYPE = DOC_ITEM_TYPE
|
||||
docs.DOC_LIST_TYPE = DOC_LIST_TYPE
|
||||
|
||||
local target
|
||||
|
||||
local function doc(key, name, desc)
|
||||
---@class pocket_doc_item
|
||||
local item = { key = key, name = name, desc = desc }
|
||||
local function sect(name)
|
||||
---@class pocket_doc_sect
|
||||
local item = { type = DOC_ITEM_TYPE.SECTION, name = name }
|
||||
table.insert(target, item)
|
||||
end
|
||||
|
||||
---@param key string item identifier for linking
|
||||
---@param name string item name for display
|
||||
---@param text_a string text body, or the subtitle/note if text_b is specified
|
||||
---@param text_b? string text body if subtitle/note was specified
|
||||
local function doc(key, name, text_a, text_b)
|
||||
if text_b == nil then
|
||||
text_b = text_a
|
||||
---@diagnostic disable-next-line: cast-local-type
|
||||
text_a = nil
|
||||
end
|
||||
|
||||
---@class pocket_doc_subsect
|
||||
local item = { type = DOC_ITEM_TYPE.SUBSECTION, key = key, name = name, subtitle = text_a, body = text_b }
|
||||
table.insert(target, item)
|
||||
end
|
||||
|
||||
local function text(body)
|
||||
---@class pocket_doc_text
|
||||
local item = { type = DOC_ITEM_TYPE.TEXT, text = body }
|
||||
table.insert(target, item)
|
||||
end
|
||||
|
||||
---@param type DOC_LIST_TYPE
|
||||
---@param items table
|
||||
---@param colors table|nil colors for indicators or nil for normal lists
|
||||
local function list(type, items, colors)
|
||||
---@class pocket_doc_list
|
||||
local list_def = { type = DOC_ITEM_TYPE.LIST, list_type = type, items = items, colors = colors }
|
||||
table.insert(target, list_def)
|
||||
end
|
||||
|
||||
-- important to note in the future: The PLC should always be in a chunk with the reactor to ensure it can protect it on chunk load if you do not keep it all chunk loaded
|
||||
|
||||
docs.alarms = {}
|
||||
@@ -28,15 +80,20 @@ doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due
|
||||
|
||||
docs.annunc = {
|
||||
unit = {
|
||||
main_section = {}, rps_section = {}, rcs_section = {}, fac_section = {}
|
||||
main_section = {}, rps_section = {}, rcs_section = {}
|
||||
},
|
||||
facility = {
|
||||
main_section = {}
|
||||
}
|
||||
}
|
||||
|
||||
target = docs.annunc.unit.main_section
|
||||
sect("Unit Status")
|
||||
doc("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
|
||||
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.")
|
||||
doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.")
|
||||
doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.")
|
||||
sect("Safety Status")
|
||||
doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is holding the reactor SCRAM'd.")
|
||||
doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.")
|
||||
doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.")
|
||||
@@ -78,21 +135,112 @@ doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity,
|
||||
doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.")
|
||||
doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.")
|
||||
|
||||
target = docs.annunc.unit.fac_section
|
||||
doc("?", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
|
||||
doc("?", "Radiation Monitor", "At least one facility radiation monitor is connected")
|
||||
doc("?", "Induction Matrix", "The induction matrix is connected.")
|
||||
doc("?", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
|
||||
doc("?", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
|
||||
doc("?", "Process Active", "Automatic process control is active.")
|
||||
doc("?", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).")
|
||||
doc("?", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
|
||||
doc("?", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
|
||||
doc("?", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
|
||||
doc("?", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
|
||||
doc("?", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
|
||||
doc("?", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
|
||||
doc("?", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
|
||||
target = docs.annunc.facility.main_section
|
||||
sect("Connectivity")
|
||||
doc("all_sys_ok", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
|
||||
doc("rad_computed_status", "Radiation Monitor", "At least one facility radiation monitor is connected")
|
||||
doc("im_computed_status", "Induction Matrix", "The induction matrix is connected.")
|
||||
doc("sps_computed_status", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
|
||||
sect("Automatic Control")
|
||||
doc("auto_ready", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
|
||||
doc("auto_active", "Process Active", "Automatic process control is active.")
|
||||
doc("auto_ramping", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).")
|
||||
doc("auto_saturated", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
|
||||
sect("Automatic SCRAM")
|
||||
doc("auto_scram", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
|
||||
doc("as_matrix_dc", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
|
||||
doc("as_matrix_fill", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
|
||||
doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
|
||||
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
|
||||
doc("as_gen_fault", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
|
||||
|
||||
docs.fp = {
|
||||
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}
|
||||
}
|
||||
|
||||
--comp id "This must never be the identical between devices, and that can only happen if you duplicate a computer (such as middle-click on it and place it elsewhere in creative mode)."
|
||||
|
||||
target = docs.fp.common
|
||||
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.")
|
||||
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")
|
||||
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.")
|
||||
|
||||
target = docs.fp.r_plc
|
||||
sect("Core Status")
|
||||
doc("fp_status", "STATUS", "This is green once the PLC is initialized and OK (has all its peripherals) and red if something is wrong, in which case you should refer to the other indicator lights (REACTOR & MODEM).")
|
||||
sect("Hardware & Network")
|
||||
doc("fp_rplc_reactor", "REACTOR", "This indicates the status of the connected reactor peripheral.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "unformed", "ok" }, { colors.red, colors.yellow, colors.green })
|
||||
doc("fp_nt_collision", "NT COLLISION", "(color accessibility modes only)", "This indicates the Reactor PLC unit ID is a duplicate of another already connected Reactor PLC.")
|
||||
sect("Co-Routine States")
|
||||
doc("fp_rplc_rt_main", "RT MAIN", "This lights up as long as the device's main loop co-routine is running, which it should be as long as STATUS is green.")
|
||||
doc("fp_rplc_rt_rps", "RT RPS", "This should always be lit up if a reactor is connected as it indicates the RPS co-routine is running, otherwise safety checks will not be running.")
|
||||
doc("fp_rplc_rt_ctx", "RT COMMS TX", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the communications transmission co-routine is running.")
|
||||
doc("fp_rplc_rt_crx", "RT COMMS RX", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the communications receiver/handler co-routine is running.")
|
||||
doc("fp_rplc_rt_spctl", "RT SPCTL", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the process setpoint controller co-routine is running.")
|
||||
sect("Status")
|
||||
doc("fp_rct_active", "RCT ACTIVE", "The reactor is active (running).")
|
||||
doc("fp_emer_cool", "EMER COOLANT", "This is only present if PLC-controlled emergency coolant is configured on that device. When lit, it indicates that it has been activated.")
|
||||
doc("fp_rps_trip", "RPS TRIP", "Flashes when the RPS has SCRAM'd the reactor due to a safety trip.")
|
||||
sect("RPS Conditions")
|
||||
doc("fp_rps_man", "MANUAL", "The RPS was tripped manually (SCRAM by user, not via the Mekanism Reactor UI).")
|
||||
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the supervisor automatically.")
|
||||
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the supervisor connection.")
|
||||
doc("fp_rps_pflt", "PLC FAULT", "The RPS tripped due to a peripheral error.")
|
||||
doc("fp_rps_rflt", "RCT FAULT", "The RPS tripped due to the reactor not being formed.")
|
||||
doc("fp_rps_temp", "HI DAMAGE", "The RPS tripped due to being >=" .. const.RPS_LIMITS.MAX_DAMAGE_PERCENT .. "% damaged.")
|
||||
doc("fp_rps_temp", "HI TEMP", "The RPS tripped due to high reactor temperature (>=" .. const.RPS_LIMITS.MAX_DAMAGE_TEMPERATURE .. "K).")
|
||||
doc("fp_rps_fuel", "LO FUEL", "The RPS tripped due to having no fuel.")
|
||||
doc("fp_rps_waste", "HI WASTE", "The RPS tripped due to having high levels of waste (>" .. (const.RPS_LIMITS.MAX_WASTE_FILL * 100) .. "%).")
|
||||
doc("fp_rps_ccool", "LO CCOOLANT", "The RPS tripped due to having low levels of cooled coolant (<" .. (const.RPS_LIMITS.MIN_COOLANT_FILL * 100) .. "%).")
|
||||
doc("fp_rps_ccool", "HI HCOOLANT", "The RPS tripped due to having high levels of heated coolant (>" .. (const.RPS_LIMITS.MAX_HEATED_COLLANT_FILL * 100) .. "%).")
|
||||
|
||||
target = docs.fp.rtu_gw
|
||||
sect("Co-Routine States")
|
||||
doc("fp_rtu_rt_main", "RT MAIN", "This indicates if the device's main loop co-routine is running.")
|
||||
doc("fp_rtu_rt_comms", "RT COMMS", "This indicates if the communications handler co-routine is running.")
|
||||
sect("Device List")
|
||||
doc("fp_rtu_rt", "RT", "In each RTU entry row, an RT light indicates if the co-routine for that RTU unit is running. This is never lit for redstone units.")
|
||||
doc("fp_rtu_rt", "Device Status", "In each RTU entry row, the light to the left of the device name indicates its peripheral status.")
|
||||
list(DOC_LIST_TYPE.LED, { "disconnected", "faulted", "unformed", "ok" }, { colors.red, colors.orange, colors.yellow, colors.green })
|
||||
text("Note that disconnected devices lack detailed information and will not be modifiable in configuration until re-connected.")
|
||||
doc("fp_rtu_rt", "Device Assignment", "In each RTU entry row, the device identification is to the right of the status light. This begins with the device type and its index followed by its assignment after the \x1a, which is a unit or the facility (FACIL). Unit 1's 3rd turbine would show up as 'TURBINE 3 \x1a UNIT 1'.")
|
||||
|
||||
target = docs.fp.supervisor
|
||||
sect("Round Trip Times")
|
||||
doc("fp_sv_fw", "RTT", "Each connection has a round trip time, or RTT. Since the supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
|
||||
list(DOC_LIST_TYPE.BULLET, { "green: <=300ms", "yellow: <=500ms ", "red: >500ms" })
|
||||
sect("SVR Tab")
|
||||
text("This tab includes information about the supervisor, covered by 'Common Items'.")
|
||||
sect("PLC Tab")
|
||||
text("This tab lists the expected PLC connections based on the number of configured units. Status information about each connection is shown when linked.")
|
||||
doc("fp_sv_link", "LINK", "This indicates if the reactor PLC is linked.")
|
||||
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the reactor PLC, or --- if disconnected.")
|
||||
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the reactor PLC.")
|
||||
sect("RTU Tab")
|
||||
text("As RTU gateways connect to the supervisor, they will show up here along with some information.")
|
||||
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU gateway.")
|
||||
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU gateway (each line on the RTU gateway's front panel).")
|
||||
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU gateway.")
|
||||
sect("PKT Tab")
|
||||
text("As pocket computers connect to the supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
|
||||
sect("DEV Tab")
|
||||
text("If nothing is connected, this will list all the expected RTU devices that aren't found. This page should be blank if everything is connected and configured correctly. If not, it will list certain types of detectable problems.")
|
||||
doc("fp_sv_d_miss", "MISSING", "These items list missing devices, with the details that should be used in the RTU's configuration.")
|
||||
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
|
||||
doc("fp_sv_d_dupe", "DUPLICATE", "If a device tries to connect that is configured the same as another, it will be rejected and show up here. If you try to connect two #1 turbines for a unit, that would fail and one would appear here.")
|
||||
sect("INF Tab")
|
||||
text("This tab gives information about the other tabs, along with extra details on the DEV tab.")
|
||||
|
||||
docs.glossary = {
|
||||
abbvs = {}, terms = {}
|
||||
@@ -106,13 +254,14 @@ doc("G_DBG", "DBG", "Debug. Abbreviation for the debugging sessions from pocket
|
||||
doc("G_FP", "FP", "Front Panel. See Terminology.")
|
||||
doc("G_Hi", "Hi", "High.")
|
||||
doc("G_Lo", "Lo", "Low.")
|
||||
doc("G_PID", "PID", "A Proportional Integral Derivitave closed-loop controller.")
|
||||
doc("G_PID", "PID", "A Proportional Integral Derivative closed-loop controller.")
|
||||
doc("G_PKT", "PKT", "Pocket. Abbreviation for the pocket computer.")
|
||||
doc("G_PLC", "PLC", "Programmable Logic Controller. A device that not only reports data and controls outputs, but can also make decisions on its own.")
|
||||
doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer created for this project that prevents peripheral calls from crashing applications.")
|
||||
doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.")
|
||||
doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).")
|
||||
doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.")
|
||||
doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.")
|
||||
doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.")
|
||||
doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.")
|
||||
doc("G_SVR", "SVR", "Supervisor. Abbreviation for the supervisory computer.")
|
||||
|
||||
@@ -7,6 +7,7 @@ local util = require("scada-common.util")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
|
||||
local control_app = require("pocket.ui.apps.control")
|
||||
local diag_apps = require("pocket.ui.apps.diag_apps")
|
||||
local dummy_app = require("pocket.ui.apps.dummy_app")
|
||||
local guide_app = require("pocket.ui.apps.guide")
|
||||
@@ -42,7 +43,7 @@ local function init(main)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- window header message and connection status
|
||||
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",alignment=ALIGN.LEFT,fg_bg=style.header}
|
||||
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
|
||||
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
|
||||
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
|
||||
|
||||
@@ -62,6 +63,7 @@ local function init(main)
|
||||
-- create all the apps & pages
|
||||
home_page(page_div)
|
||||
unit_app(page_div)
|
||||
control_app(page_div)
|
||||
guide_app(page_div)
|
||||
loader_app(page_div)
|
||||
sys_apps(page_div)
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local docs = require("pocket.ui.docs")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
local cpair = core.cpair
|
||||
|
||||
local DOC_TYPE = docs.DOC_ITEM_TYPE
|
||||
local LIST_TYPE = docs.DOC_LIST_TYPE
|
||||
|
||||
-- new guide documentation section
|
||||
---@param data _guide_section_constructor_data
|
||||
---@param base_page nav_tree_page
|
||||
@@ -34,33 +42,104 @@ 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=3,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=30,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
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 _end
|
||||
local sect_id = 1
|
||||
local page_end
|
||||
|
||||
for i = 1, #items do
|
||||
local item = items[i] ---@type pocket_doc_item
|
||||
local item = items[i] ---@type pocket_doc_sect|pocket_doc_subsect|pocket_doc_text|pocket_doc_list
|
||||
|
||||
local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)}
|
||||
TextBox{parent=def_list,text=item.desc}
|
||||
_end = Div{parent=def_list,height=1,can_focus=true}
|
||||
if item.type == DOC_TYPE.SECTION then
|
||||
---@cast item pocket_doc_sect
|
||||
|
||||
local function view()
|
||||
_end.focus()
|
||||
view_page.nav_to()
|
||||
anchor.focus()
|
||||
local title_text = sect_id.."."
|
||||
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)}
|
||||
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}
|
||||
|
||||
local function view()
|
||||
page_end.focus()
|
||||
view_page.nav_to()
|
||||
anchor.focus()
|
||||
end
|
||||
|
||||
if #name_list.get_children() > 0 then
|
||||
local _ = Div{parent=name_list,height=1}
|
||||
end
|
||||
|
||||
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)}
|
||||
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
|
||||
elseif item.type == DOC_TYPE.SUBSECTION then
|
||||
---@cast item pocket_doc_subsect
|
||||
|
||||
local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)}
|
||||
|
||||
if item.subtitle then
|
||||
TextBox{parent=def_list,text=item.subtitle,fg_bg=cpair(colors.gray,colors.black)}
|
||||
end
|
||||
|
||||
TextBox{parent=def_list,text=item.body}
|
||||
|
||||
page_end = Div{parent=def_list,height=1,can_focus=true}
|
||||
|
||||
local function view()
|
||||
page_end.focus()
|
||||
view_page.nav_to()
|
||||
anchor.focus()
|
||||
end
|
||||
|
||||
doc_map[item.key] = view
|
||||
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)}
|
||||
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
|
||||
|
||||
TextBox{parent=def_list,text=item.text}
|
||||
|
||||
page_end = Div{parent=def_list,height=1,can_focus=true}
|
||||
elseif item.type == DOC_TYPE.LIST then
|
||||
---@cast item pocket_doc_list
|
||||
|
||||
local container = Div{parent=def_list,height=#item.items}
|
||||
|
||||
if item.list_type == LIST_TYPE.BULLET then
|
||||
for _, li in ipairs(item.items) do
|
||||
TextBox{parent=container,x=2,text="\x07 "..li}
|
||||
end
|
||||
elseif item.list_type == LIST_TYPE.NUMBERED then
|
||||
local width = string.len("" .. #item.items)
|
||||
for idx, li in ipairs(item.items) do
|
||||
TextBox{parent=container,x=2,text=util.sprintf("%" .. width .. "d. %s", idx, li)}
|
||||
end
|
||||
elseif item.list_type == LIST_TYPE.INDICATOR then
|
||||
for idx, li in ipairs(item.items) do
|
||||
local _ = IndicatorLight{parent=container,x=2,label=li,colors=cpair(colors.black,item.colors[idx])}
|
||||
end
|
||||
elseif item.list_type == LIST_TYPE.LED then
|
||||
for idx, li in ipairs(item.items) do
|
||||
local _ = LED{parent=container,x=2,label=li,colors=cpair(colors.black,item.colors[idx])}
|
||||
end
|
||||
end
|
||||
|
||||
page_end = Div{parent=def_list,height=1,can_focus=true}
|
||||
end
|
||||
|
||||
doc_map[item.key] = view
|
||||
table.insert(search_db, { string.lower(item.name), item.name, title, view })
|
||||
|
||||
PushButton{parent=name_list,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
|
||||
|
||||
if i % 12 == 0 then util.nop() end
|
||||
end
|
||||
|
||||
log.debug("guide section " .. title .. " generated with final height ".. _end.get_y())
|
||||
log.debug("guide section " .. title .. " generated with final height ".. page_end.get_y())
|
||||
|
||||
util.nop()
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ local function new_view(root)
|
||||
|
||||
App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=16,y=7,text="\x08",title="Devices",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
|
||||
|
||||
@@ -59,13 +59,13 @@ return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update)
|
||||
ccool.register(ps, "energy_fill", ccool.update)
|
||||
|
||||
TextBox{parent=tbn_div,text="Production",x=3,y=3,width=17,fg_bg=label}
|
||||
local prod_rate = PowerIndicator{parent=tbn_div,x=3,y=4,lu_colors=lu_col,label="",format="%11.2f",value=0,rate=true,width=17,fg_bg=text_fg}
|
||||
local prod_rate = PowerIndicator{parent=tbn_div,x=3,y=4,lu_colors=lu_col,label="",unit=db.energy_label,format="%11.2f",value=0,rate=true,width=17,fg_bg=text_fg}
|
||||
TextBox{parent=tbn_div,text="Flow Rate",x=3,y=5,width=17,fg_bg=label}
|
||||
local flow_rate = DataIndicator{parent=tbn_div,x=3,y=6,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=17,fg_bg=text_fg}
|
||||
TextBox{parent=tbn_div,text="Steam Input Rate",x=3,y=7,width=17,fg_bg=label}
|
||||
local input_rate = DataIndicator{parent=tbn_div,x=3,y=8,lu_colors=lu_col,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=17,fg_bg=text_fg}
|
||||
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(db.energy_convert(val)) end)
|
||||
flow_rate.register(ps, "flow_rate", flow_rate.update)
|
||||
input_rate.register(ps, "steam_input_rate", input_rate.update)
|
||||
|
||||
@@ -99,10 +99,10 @@ return function (app, u_page, panes, tbn_pane, u_id, t_id, ps, update)
|
||||
|
||||
TextBox{parent=tbn_ext_div,text="Energy Fill",x=1,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="",format="%17.4f",value=0,width=21,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}
|
||||
|
||||
charge_p.register(ps, "energy_fill", function (x) charge_p.update(x * 100) end)
|
||||
charge_amnt.register(ps, "energy", charge_amnt.update)
|
||||
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}
|
||||
|
||||
239
reactor-plc/config/check.lua
Normal file
239
reactor-plc/config/check.lua
Normal file
@@ -0,0 +1,239 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local plc = require("reactor-plc.plc")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
local self = {
|
||||
nic = nil, ---@type nic
|
||||
net_listen = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = util.time_ms() * 10,
|
||||
|
||||
self_check_pass = true,
|
||||
|
||||
settings = nil, ---@type plc_config
|
||||
|
||||
run_test_btn = nil, ---@type graphics_element
|
||||
sc_log = nil, ---@type graphics_element
|
||||
self_check_msg = nil ---@type function
|
||||
}
|
||||
|
||||
-- report successful completion of the check
|
||||
local function check_complete()
|
||||
TextBox{parent=self.sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)}
|
||||
TextBox{parent=self.sc_log,text=""}
|
||||
local more = Div{parent=self.sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)}
|
||||
TextBox{parent=more,text="if you still have a problem:"}
|
||||
TextBox{parent=more,text="- check the wiki on GitHub"}
|
||||
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
|
||||
end
|
||||
|
||||
-- send a management packet to the supervisor
|
||||
---@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()
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||
|
||||
self.nic.transmit(self.settings.SVR_Channel, self.settings.PLC_Channel, s_pkt)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- handle an establish message from the supervisor
|
||||
---@param packet mgmt_frame
|
||||
local function handle_packet(packet)
|
||||
local error_msg = nil
|
||||
|
||||
if packet.scada_frame.local_channel() ~= self.settings.PLC_Channel then
|
||||
error_msg = "error: unknown receive channel"
|
||||
elseif packet.scada_frame.remote_channel() == self.settings.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
if packet.type == MGMT_TYPE.ESTABLISH then
|
||||
if packet.length == 1 then
|
||||
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
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
error_msg = "error: supervisor connection denied"
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
error_msg = "another reactor PLC is connected with this reactor unit ID"
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update ...)"
|
||||
else
|
||||
error_msg = "error: invalid reply from supervisor"
|
||||
end
|
||||
else
|
||||
error_msg = "error: invalid reply length from supervisor"
|
||||
end
|
||||
else
|
||||
error_msg = "error: didn't get an establish reply from supervisor"
|
||||
end
|
||||
end
|
||||
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
|
||||
if error_msg then
|
||||
self.self_check_msg(nil, false, error_msg)
|
||||
end
|
||||
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")
|
||||
end
|
||||
|
||||
-- execute the self-check
|
||||
local function self_check()
|
||||
self.run_test_btn.disable()
|
||||
|
||||
self.sc_log.remove_all()
|
||||
ppm.mount_all()
|
||||
|
||||
self.self_check_pass = true
|
||||
|
||||
local modem = ppm.get_wireless_modem()
|
||||
local reactor = ppm.get_fission_reactor()
|
||||
local valid_cfg = plc.validate_config(self.settings)
|
||||
|
||||
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
|
||||
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 configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones")
|
||||
|
||||
if valid_cfg and modem then
|
||||
self.self_check_msg("> check supervisor connection...")
|
||||
|
||||
-- init mac as needed
|
||||
if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then
|
||||
network.init_mac(self.settings.AuthKey)
|
||||
else
|
||||
network.deinit_mac()
|
||||
end
|
||||
|
||||
self.nic = network.nic(modem)
|
||||
|
||||
self.nic.closeAll()
|
||||
self.nic.open(self.settings.PLC_Channel)
|
||||
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.net_listen = true
|
||||
|
||||
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, self.settings.UnitID })
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
else
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
end
|
||||
end
|
||||
|
||||
-- exit self check back home
|
||||
---@param main_pane graphics_element
|
||||
local function exit_self_check(main_pane)
|
||||
tcd.abort(handle_timeout)
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
self.sc_log.remove_all()
|
||||
main_pane.set_value(1)
|
||||
end
|
||||
|
||||
local check = {}
|
||||
|
||||
-- create the self-check view
|
||||
---@param main_pane graphics_element
|
||||
---@param settings_cfg plc_config
|
||||
---@param check_sys graphics_element
|
||||
---@param style table
|
||||
function check.create(main_pane, settings_cfg, check_sys, style)
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
self.settings = settings_cfg
|
||||
|
||||
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}
|
||||
|
||||
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)}
|
||||
|
||||
local last_check = { nil, nil }
|
||||
|
||||
function self.self_check_msg(msg, success, fail_msg)
|
||||
if type(msg) == "string" then
|
||||
last_check[1] = Div{parent=self.sc_log,height=1}
|
||||
local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg}
|
||||
last_check[2] = e.get_x()+string.len(msg)
|
||||
end
|
||||
|
||||
if type(fail_msg) == "string" then
|
||||
TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))}
|
||||
|
||||
if not success then
|
||||
local fail = Div{parent=self.sc_log,height=#util.strwrap(fail_msg, 46)}
|
||||
TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
|
||||
self.self_check_pass = self.self_check_pass and success
|
||||
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}
|
||||
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
|
||||
|
||||
-- handle incoming modem messages
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@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)
|
||||
|
||||
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
tcd.abort(handle_timeout)
|
||||
handle_packet(mgmt_pkt.get())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return check
|
||||
620
reactor-plc/config/system.lua
Normal file
620
reactor-plc/config/system.lua
Normal file
@@ -0,0 +1,620 @@
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local CheckBox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local Radio2D = require("graphics.elements.controls.radio_2d")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
local TextField = require("graphics.elements.form.text_field")
|
||||
|
||||
local IndLight = require("graphics.elements.indicators.light")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
local self = {
|
||||
importing_legacy = false,
|
||||
|
||||
set_networked = nil, ---@type function
|
||||
bundled_emcool = nil, ---@type function
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type graphics_element
|
||||
auth_key_textbox = nil, ---@type graphics_element
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
|
||||
local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
|
||||
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
|
||||
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
|
||||
|
||||
-- convert text representation to index
|
||||
---@param side string
|
||||
local function side_to_idx(side)
|
||||
for k, v in ipairs(side_options_map) do
|
||||
if v == side then return k end
|
||||
end
|
||||
end
|
||||
|
||||
-- convert color to index
|
||||
---@param color color
|
||||
local function color_to_idx(color)
|
||||
for k, v in ipairs(color_options_map) do
|
||||
if v == color then return k end
|
||||
end
|
||||
end
|
||||
|
||||
local system = {}
|
||||
|
||||
-- create the system configuration view
|
||||
---@param tool_ctl _plc_cfg_tool_ctl
|
||||
---@param main_pane graphics_element
|
||||
---@param cfg_sys table
|
||||
---@param divs table
|
||||
---@param style table
|
||||
---@param exit function
|
||||
function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
---@type plc_config, plc_config, plc_config, table, function
|
||||
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = table.unpack(cfg_sys)
|
||||
|
||||
---@type graphics_element, graphics_element, graphics_element, graphics_element, graphics_element
|
||||
local plc_cfg, net_cfg, log_cfg, clr_cfg, summary = table.unpack(divs)
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
--#region PLC
|
||||
|
||||
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_4 = 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}}
|
||||
|
||||
TextBox{parent=plc_cfg,x=1,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}
|
||||
|
||||
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 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,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,x=1,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}
|
||||
|
||||
function self.set_networked(enable)
|
||||
tmp_cfg.Networked = enable
|
||||
if enable then u_id.set_max(4) else u_id.set_max(999) end
|
||||
end
|
||||
|
||||
local function submit_id()
|
||||
local unit_id = tonumber(u_id.get_value())
|
||||
if unit_id ~= nil then
|
||||
u_id_err.hide(true)
|
||||
tmp_cfg.UnitID = unit_id
|
||||
plc_pane.set_value(3)
|
||||
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,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}
|
||||
|
||||
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 function next_from_plc()
|
||||
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end
|
||||
end
|
||||
|
||||
local function submit_en_emcool()
|
||||
tmp_cfg.EmerCoolEnable = en_em_cool.get_value()
|
||||
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,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,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}
|
||||
if ini_cfg.EmerCoolColor == nil then color.disable() end
|
||||
|
||||
function self.bundled_emcool(en) if en then color.enable() else color.disable() end end
|
||||
|
||||
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)
|
||||
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,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Network
|
||||
|
||||
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_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,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,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 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 submit_channels()
|
||||
local svr_c = tonumber(svr_chan.get_value())
|
||||
local plc_c = tonumber(plc_chan.get_value())
|
||||
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)
|
||||
chan_err.hide(true)
|
||||
elseif svr_c == nil then
|
||||
chan_err.set_value("Please set the supervisor channel.")
|
||||
chan_err.show()
|
||||
else
|
||||
chan_err.set_value("Please set the PLC channel.")
|
||||
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}
|
||||
|
||||
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_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}
|
||||
|
||||
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 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()
|
||||
else
|
||||
p2_err.set_value("Please set the trusted range.")
|
||||
p2_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_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 compution (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, _, censor = 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}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(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}
|
||||
|
||||
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 function submit_auth()
|
||||
local v = key.get_value()
|
||||
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(4)
|
||||
key_err.hide(true)
|
||||
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}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Logging
|
||||
|
||||
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_c_1,x=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,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}
|
||||
|
||||
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)}
|
||||
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}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
path_err.hide(true)
|
||||
tmp_cfg.LogMode = mode.get_value() - 1
|
||||
tmp_cfg.LogPath = path.get_value()
|
||||
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||
tool_ctl.color_apply.hide(true)
|
||||
tool_ctl.color_next.show()
|
||||
main_pane.set_value(5)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
local function back_from_log()
|
||||
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,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Color Options
|
||||
|
||||
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
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}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,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,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_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,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
|
||||
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
|
||||
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
|
||||
|
||||
local function recolor(value)
|
||||
local c = themes.smooth_stone.color_modes[value]
|
||||
|
||||
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
|
||||
b_off.hide()
|
||||
g_off.show()
|
||||
else
|
||||
g_off.hide()
|
||||
b_off.show()
|
||||
end
|
||||
|
||||
if #c == 0 then
|
||||
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
|
||||
else
|
||||
term.setPaletteColor(colors.green, c[1].hex)
|
||||
term.setPaletteColor(colors.yellow, c[2].hex)
|
||||
term.setPaletteColor(colors.red, c[3].hex)
|
||||
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,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))
|
||||
tool_ctl.jumped_to_color = false
|
||||
recolor(1)
|
||||
end
|
||||
|
||||
local function show_access()
|
||||
clr_pane.set_value(2)
|
||||
recolor(c_mode.get_value())
|
||||
end
|
||||
|
||||
local function submit_colors()
|
||||
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
|
||||
tmp_cfg.ColorMode = c_mode.get_value()
|
||||
|
||||
if tool_ctl.jumped_to_color then
|
||||
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
|
||||
settings.set("ColorMode", tmp_cfg.ColorMode)
|
||||
|
||||
if settings.save("/reactor-plc.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
clr_pane.set_value(3)
|
||||
else
|
||||
clr_pane.set_value(4)
|
||||
end
|
||||
else
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
self.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(6)
|
||||
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,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)
|
||||
|
||||
local function c_go_home()
|
||||
main_pane.set_value(1)
|
||||
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)}
|
||||
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)}
|
||||
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
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
|
||||
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}}
|
||||
|
||||
TextBox{parent=summary,x=1,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 function back_from_settings()
|
||||
if tool_ctl.viewing_config or self.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
self.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(5)
|
||||
end
|
||||
end
|
||||
|
||||
---@param element graphics_element
|
||||
---@param data any
|
||||
local function try_set(element, data)
|
||||
if data ~= nil then element.set_value(data) end
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
|
||||
if settings.save("/reactor-plc.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(networked, ini_cfg.Networked)
|
||||
try_set(u_id, ini_cfg.UnitID)
|
||||
try_set(en_em_cool, ini_cfg.EmerCoolEnable)
|
||||
try_set(side, side_to_idx(ini_cfg.EmerCoolSide))
|
||||
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(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(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
try_set(fp_theme, ini_cfg.FrontPanelTheme)
|
||||
try_set(c_mode, ini_cfg.ColorMode)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
tool_ctl.color_cfg.enable()
|
||||
|
||||
if self.importing_legacy then
|
||||
self.importing_legacy = false
|
||||
sum_pane.set_value(3)
|
||||
else
|
||||
sum_pane.set_value(2)
|
||||
end
|
||||
else
|
||||
sum_pane.set_value(4)
|
||||
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}
|
||||
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!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
plc_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
clr_pane.set_value(1)
|
||||
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,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."}
|
||||
|
||||
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,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}
|
||||
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
|
||||
|
||||
--#region Tool Functions
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("reactor-plc.config")
|
||||
|
||||
tmp_cfg.Networked = config.NETWORKED
|
||||
tmp_cfg.UnitID = config.REACTOR_ID
|
||||
tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table"
|
||||
|
||||
if tmp_cfg.EmerCoolEnable then
|
||||
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
|
||||
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
|
||||
else
|
||||
tmp_cfg.EmerCoolSide = nil
|
||||
tmp_cfg.EmerCoolColor = nil
|
||||
end
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
|
||||
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
|
||||
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||
tmp_cfg.LogMode = config.LOG_MODE
|
||||
tmp_cfg.LogPath = config.LOG_PATH
|
||||
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(6)
|
||||
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 plc_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
setting_list.remove_all()
|
||||
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
if cfg.AuthKey then self.show_key_btn.enable() else self.show_key_btn.disable() end
|
||||
self.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
local f = fields[i]
|
||||
local height = 1
|
||||
local label_w = string.len(f[2])
|
||||
local val_max_w = (inner_width - label_w) + 1
|
||||
local raw = cfg[f[1]]
|
||||
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] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
val = util.strval(themes.fp_theme_name(raw))
|
||||
elseif f[1] == "ColorMode" then
|
||||
val = util.strval(themes.color_mode_name(raw))
|
||||
end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
local lines = util.strwrap(val, inner_width)
|
||||
height = #lines + 1
|
||||
end
|
||||
|
||||
local line = Div{parent=setting_list,height=height,fg_bg=c}
|
||||
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
return system
|
||||
@@ -2,38 +2,30 @@
|
||||
-- Configuration GUI
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
local check = require("reactor-plc.config.check")
|
||||
local system = require("reactor-plc.config.system")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local core = require("graphics.core")
|
||||
local themes = require("graphics.themes")
|
||||
|
||||
local CheckBox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local Radio2D = require("graphics.elements.controls.radio_2d")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
local TextField = require("graphics.elements.form.text_field")
|
||||
|
||||
local IndLight = require("graphics.elements.indicators.light")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local println = util.println
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {
|
||||
@@ -48,22 +40,22 @@ local configurator = {}
|
||||
|
||||
local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
|
||||
style.colors = themes.smooth_stone.colors
|
||||
style.colors = themes.smooth_stone.colors
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local nav_fg_bg = bw_fg_bg
|
||||
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
style.bw_fg_bg = cpair(colors.black, colors.white)
|
||||
style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
style.nav_fg_bg = style.bw_fg_bg
|
||||
style.btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
|
||||
|
||||
---@class _plc_cfg_tool_ctl
|
||||
local tool_ctl = {
|
||||
ask_config = false,
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
importing_legacy = false,
|
||||
jumped_to_color = false,
|
||||
|
||||
view_cfg = nil, ---@type graphics_element
|
||||
@@ -72,16 +64,8 @@ local tool_ctl = {
|
||||
color_apply = nil, ---@type graphics_element
|
||||
settings_apply = nil, ---@type graphics_element
|
||||
|
||||
set_networked = nil, ---@type function
|
||||
bundled_emcool = nil, ---@type function
|
||||
gen_summary = nil, ---@type function
|
||||
show_current_cfg = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type graphics_element
|
||||
auth_key_textbox = nil, ---@type graphics_element
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
---@class plc_config
|
||||
@@ -127,27 +111,6 @@ local fields = {
|
||||
{ "ColorMode", "Color Mode", themes.COLOR_MODE.STANDARD }
|
||||
}
|
||||
|
||||
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
|
||||
local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
|
||||
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
|
||||
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
|
||||
|
||||
-- convert text representation to index
|
||||
---@param side string
|
||||
local function side_to_idx(side)
|
||||
for k, v in ipairs(side_options_map) do
|
||||
if v == side then return k end
|
||||
end
|
||||
end
|
||||
|
||||
-- convert color to index
|
||||
---@param color color
|
||||
local function color_to_idx(color)
|
||||
for k, v in ipairs(color_options_map) do
|
||||
if v == color then return k end
|
||||
end
|
||||
end
|
||||
|
||||
-- load data from the settings file
|
||||
---@param target plc_config
|
||||
---@param raw boolean? true to not use default values
|
||||
@@ -164,6 +127,11 @@ end
|
||||
-- create the config view
|
||||
---@param display graphics_element
|
||||
local function config_view(display)
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local g_lg_fg_bg = style.g_lg_fg_bg
|
||||
local nav_fg_bg = style.nav_fg_bg
|
||||
local btn_act_fg_bg = style.btn_act_fg_bg
|
||||
local btn_dis_fg_bg = style.btn_dis_fg_bg
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function exit() os.queueEvent("terminate") end
|
||||
@@ -179,17 +147,18 @@ local function config_view(display)
|
||||
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_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog}}
|
||||
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}}
|
||||
|
||||
-- Main Page
|
||||
--#region Main Page
|
||||
|
||||
local y_start = 5
|
||||
|
||||
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
end
|
||||
|
||||
@@ -206,7 +175,7 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
|
||||
local function jump_color()
|
||||
tool_ctl.jumped_to_color = true
|
||||
@@ -216,7 +185,8 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
PushButton{parent=main_page,x=10,y=17,min_width=12,text="Self-Check",callback=function()main_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
tool_ctl.color_cfg = PushButton{parent=main_page,x=23,y=17,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
if not tool_ctl.has_config then
|
||||
@@ -224,439 +194,18 @@ local function config_view(display)
|
||||
tool_ctl.color_cfg.disable()
|
||||
end
|
||||
|
||||
--#region PLC
|
||||
--#endregion
|
||||
|
||||
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||
--#region System Configuration
|
||||
|
||||
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
|
||||
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
|
||||
local divs = { plc_cfg, net_cfg, log_cfg, clr_cfg, summary }
|
||||
|
||||
TextBox{parent=plc_cfg,x=1,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}
|
||||
|
||||
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 function submit_networked()
|
||||
tool_ctl.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,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,x=1,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}
|
||||
|
||||
local function submit_id()
|
||||
local unit_id = tonumber(u_id.get_value())
|
||||
if unit_id ~= nil then
|
||||
u_id_err.hide(true)
|
||||
tmp_cfg.UnitID = unit_id
|
||||
plc_pane.set_value(3)
|
||||
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,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}
|
||||
|
||||
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 function next_from_plc()
|
||||
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end
|
||||
end
|
||||
|
||||
local function submit_en_emcool()
|
||||
tmp_cfg.EmerCoolEnable = en_em_cool.get_value()
|
||||
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,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,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)tool_ctl.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}
|
||||
if ini_cfg.EmerCoolColor == nil then color.disable() end
|
||||
|
||||
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)
|
||||
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,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
system.create(tool_ctl, main_pane, settings, divs, style, exit)
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Network
|
||||
|
||||
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_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,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,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 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 submit_channels()
|
||||
local svr_c = tonumber(svr_chan.get_value())
|
||||
local plc_c = tonumber(plc_chan.get_value())
|
||||
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)
|
||||
chan_err.hide(true)
|
||||
elseif svr_c == nil then
|
||||
chan_err.set_value("Please set the supervisor channel.")
|
||||
chan_err.show()
|
||||
else
|
||||
chan_err.set_value("Please set the PLC channel.")
|
||||
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}
|
||||
|
||||
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_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}
|
||||
|
||||
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 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()
|
||||
else
|
||||
p2_err.set_value("Please set the trusted range.")
|
||||
p2_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_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 compution (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, _, censor = 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}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(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}
|
||||
|
||||
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 function submit_auth()
|
||||
local v = key.get_value()
|
||||
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(4)
|
||||
key_err.hide(true)
|
||||
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}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Logging
|
||||
|
||||
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_c_1,x=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,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}
|
||||
|
||||
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)}
|
||||
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}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
path_err.hide(true)
|
||||
tmp_cfg.LogMode = mode.get_value() - 1
|
||||
tmp_cfg.LogPath = path.get_value()
|
||||
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||
tool_ctl.color_apply.hide(true)
|
||||
tool_ctl.color_next.show()
|
||||
main_pane.set_value(5)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
local function back_from_log()
|
||||
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,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Color Options
|
||||
|
||||
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
|
||||
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}}
|
||||
|
||||
TextBox{parent=clr_cfg,x=1,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,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_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,x=21,y=7,text="Preview"}
|
||||
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
|
||||
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
|
||||
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
|
||||
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
|
||||
|
||||
local function recolor(value)
|
||||
local c = themes.smooth_stone.color_modes[value]
|
||||
|
||||
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
|
||||
b_off.hide()
|
||||
g_off.show()
|
||||
else
|
||||
g_off.hide()
|
||||
b_off.show()
|
||||
end
|
||||
|
||||
if #c == 0 then
|
||||
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
|
||||
else
|
||||
term.setPaletteColor(colors.green, c[1].hex)
|
||||
term.setPaletteColor(colors.yellow, c[2].hex)
|
||||
term.setPaletteColor(colors.red, c[3].hex)
|
||||
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,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))
|
||||
tool_ctl.jumped_to_color = false
|
||||
recolor(1)
|
||||
end
|
||||
|
||||
local function show_access()
|
||||
clr_pane.set_value(2)
|
||||
recolor(c_mode.get_value())
|
||||
end
|
||||
|
||||
local function submit_colors()
|
||||
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
|
||||
tmp_cfg.ColorMode = c_mode.get_value()
|
||||
|
||||
if tool_ctl.jumped_to_color then
|
||||
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
|
||||
settings.set("ColorMode", tmp_cfg.ColorMode)
|
||||
|
||||
if settings.save("/reactor-plc.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
clr_pane.set_value(3)
|
||||
else
|
||||
clr_pane.set_value(4)
|
||||
end
|
||||
else
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(6)
|
||||
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,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)
|
||||
|
||||
local function c_go_home()
|
||||
main_pane.set_value(1)
|
||||
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)}
|
||||
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)}
|
||||
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
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
|
||||
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}}
|
||||
|
||||
TextBox{parent=summary,x=1,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 function back_from_settings()
|
||||
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(5)
|
||||
end
|
||||
end
|
||||
|
||||
---@param element graphics_element
|
||||
---@param data any
|
||||
local function try_set(element, data)
|
||||
if data ~= nil then element.set_value(data) end
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
|
||||
if settings.save("/reactor-plc.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(networked, ini_cfg.Networked)
|
||||
try_set(u_id, ini_cfg.UnitID)
|
||||
try_set(en_em_cool, ini_cfg.EmerCoolEnable)
|
||||
try_set(side, side_to_idx(ini_cfg.EmerCoolSide))
|
||||
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(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(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
try_set(fp_theme, ini_cfg.FrontPanelTheme)
|
||||
try_set(c_mode, ini_cfg.ColorMode)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
|
||||
if tool_ctl.importing_legacy then
|
||||
tool_ctl.importing_legacy = false
|
||||
sum_pane.set_value(3)
|
||||
else
|
||||
sum_pane.set_value(2)
|
||||
end
|
||||
else
|
||||
sum_pane.set_value(4)
|
||||
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}
|
||||
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
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!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
plc_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
clr_pane.set_value(1)
|
||||
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,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."}
|
||||
|
||||
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,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}
|
||||
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
|
||||
|
||||
-- Config Change Log
|
||||
--#region Config Change Log
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||
|
||||
@@ -675,103 +224,13 @@ local function config_view(display)
|
||||
|
||||
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}
|
||||
|
||||
-- set tool functions now that we have the elements
|
||||
--#endregion
|
||||
|
||||
function tool_ctl.set_networked(enable)
|
||||
tmp_cfg.Networked = enable
|
||||
if enable then u_id.set_max(4) else u_id.set_max(999) end
|
||||
end
|
||||
--#region Self-Check
|
||||
|
||||
function tool_ctl.bundled_emcool(en) if en then color.enable() else color.disable() end end
|
||||
check.create(main_pane, settings_cfg, check_sys, style)
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("reactor-plc.config")
|
||||
|
||||
tmp_cfg.Networked = config.NETWORKED
|
||||
tmp_cfg.UnitID = config.REACTOR_ID
|
||||
tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table"
|
||||
|
||||
if tmp_cfg.EmerCoolEnable then
|
||||
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
|
||||
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
|
||||
else
|
||||
tmp_cfg.EmerCoolSide = nil
|
||||
tmp_cfg.EmerCoolColor = nil
|
||||
end
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
|
||||
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
|
||||
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||
tmp_cfg.LogMode = config.LOG_MODE
|
||||
tmp_cfg.LogPath = config.LOG_PATH
|
||||
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(6)
|
||||
tool_ctl.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function tool_ctl.show_auth_key()
|
||||
tool_ctl.show_key_btn.disable()
|
||||
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
|
||||
end
|
||||
|
||||
-- generate the summary list
|
||||
---@param cfg plc_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
setting_list.remove_all()
|
||||
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
if cfg.AuthKey then tool_ctl.show_key_btn.enable() else tool_ctl.show_key_btn.disable() end
|
||||
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
local f = fields[i]
|
||||
local height = 1
|
||||
local label_w = string.len(f[2])
|
||||
local val_max_w = (inner_width - label_w) + 1
|
||||
local raw = cfg[f[1]]
|
||||
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] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
val = util.strval(themes.fp_theme_name(raw))
|
||||
elseif f[1] == "ColorMode" then
|
||||
val = util.strval(themes.color_mode_name(raw))
|
||||
end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
local lines = util.strwrap(val, inner_width)
|
||||
height = #lines + 1
|
||||
end
|
||||
|
||||
local line = Div{parent=setting_list,height=height,fg_bg=c}
|
||||
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
--#endregion
|
||||
end
|
||||
|
||||
-- reset terminal screen
|
||||
@@ -802,7 +261,7 @@ function configurator.configure(ask_config)
|
||||
config_view(display)
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3 = util.pull_event()
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
@@ -815,6 +274,8 @@ 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 == "modem_message" then
|
||||
check.receive_sv(param1, param2, param3, param4, param5)
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
|
||||
@@ -148,8 +148,8 @@ local function init(panel)
|
||||
--
|
||||
|
||||
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,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)
|
||||
|
||||
@@ -57,41 +57,47 @@ function plc.load_config()
|
||||
config.FrontPanelTheme = settings.get("FrontPanelTheme")
|
||||
config.ColorMode = settings.get("ColorMode")
|
||||
|
||||
return plc.validate_config(config)
|
||||
end
|
||||
|
||||
-- validate a PLC configuration
|
||||
---@param cfg plc_config
|
||||
function plc.validate_config(cfg)
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_bool(config.Networked)
|
||||
cfv.assert_type_int(config.UnitID)
|
||||
cfv.assert_type_bool(config.EmerCoolEnable)
|
||||
cfv.assert_type_bool(cfg.Networked)
|
||||
cfv.assert_type_int(cfg.UnitID)
|
||||
cfv.assert_type_bool(cfg.EmerCoolEnable)
|
||||
|
||||
if config.Networked == true then
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.PLC_Channel)
|
||||
cfv.assert_type_num(config.ConnTimeout)
|
||||
cfv.assert_min(config.ConnTimeout, 2)
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
if cfg.Networked == true then
|
||||
cfv.assert_channel(cfg.SVR_Channel)
|
||||
cfv.assert_channel(cfg.PLC_Channel)
|
||||
cfv.assert_type_num(cfg.ConnTimeout)
|
||||
cfv.assert_min(cfg.ConnTimeout, 2)
|
||||
cfv.assert_type_num(cfg.TrustedRange)
|
||||
cfv.assert_min(cfg.TrustedRange, 0)
|
||||
cfv.assert_type_str(cfg.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
if type(cfg.AuthKey) == "string" then
|
||||
local len = string.len(cfg.AuthKey)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
cfv.assert_type_int(cfg.LogMode)
|
||||
cfv.assert_range(cfg.LogMode, 0, 1)
|
||||
cfv.assert_type_str(cfg.LogPath)
|
||||
cfv.assert_type_bool(cfg.LogDebug)
|
||||
|
||||
cfv.assert_type_int(config.FrontPanelTheme)
|
||||
cfv.assert_range(config.FrontPanelTheme, 1, 2)
|
||||
cfv.assert_type_int(config.ColorMode)
|
||||
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
cfv.assert_type_int(cfg.FrontPanelTheme)
|
||||
cfv.assert_range(cfg.FrontPanelTheme, 1, 2)
|
||||
cfv.assert_type_int(cfg.ColorMode)
|
||||
cfv.assert_range(cfg.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
|
||||
|
||||
-- check emergency coolant configuration if enabled
|
||||
if config.EmerCoolEnable then
|
||||
cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true)
|
||||
cfv.assert_eq(config.EmerCoolColor == nil or rsio.is_color(config.EmerCoolColor), true)
|
||||
if cfg.EmerCoolEnable then
|
||||
cfv.assert_eq(rsio.is_valid_side(cfg.EmerCoolSide), true)
|
||||
cfv.assert_eq(cfg.EmerCoolColor == nil or rsio.is_color(cfg.EmerCoolColor), true)
|
||||
end
|
||||
|
||||
return cfv.valid()
|
||||
@@ -729,6 +735,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
|
||||
-- attempt to establish link with supervisor
|
||||
function public.send_link_req()
|
||||
self.r_seq_num = nil
|
||||
_send_mgmt(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, config.UnitID })
|
||||
end
|
||||
|
||||
@@ -827,7 +834,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif self.linked and (src_addr ~= self.sv_addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
|
||||
|
||||
@@ -18,7 +18,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.0"
|
||||
local R_PLC_VERSION = "v1.8.8"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
@@ -84,7 +84,8 @@ assert(#PORT_DSGN == rsio.NUM_PORTS)
|
||||
local changes = {
|
||||
{ "v1.7.9", { "ConnTimeout can now have a fractional part" } },
|
||||
{ "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } },
|
||||
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }
|
||||
{ "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" } }
|
||||
}
|
||||
|
||||
---@class rtu_rs_definition
|
||||
@@ -158,7 +159,6 @@ local tool_ctl = {
|
||||
p_idx = nil, ---@type graphics_element
|
||||
p_unit = nil, ---@type graphics_element
|
||||
p_assign_btn = nil, ---@type graphics_element
|
||||
p_assign_end = nil, ---@type graphics_element
|
||||
p_desc = nil, ---@type graphics_element
|
||||
p_desc_ext = nil, ---@type graphics_element
|
||||
p_err = nil, ---@type graphics_element
|
||||
@@ -285,7 +285,7 @@ local function config_view(display)
|
||||
local y_start = 2
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
else
|
||||
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the RTU gateway configurator! Please select one of the following options."}
|
||||
@@ -650,8 +650,11 @@ local function config_view(display)
|
||||
|
||||
---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections
|
||||
local function save_and_continue(exclude_conns)
|
||||
for k, v in pairs(tmp_cfg) do
|
||||
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then settings.set(k, v) end
|
||||
for _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
end
|
||||
|
||||
-- always set these if missing
|
||||
@@ -828,53 +831,35 @@ local function config_view(display)
|
||||
tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':")
|
||||
tool_ctl.p_desc_ext.set_value("")
|
||||
|
||||
local function reposition(prompt, idx_x, idx_max, unit_x, unit_y, desc_y)
|
||||
tool_ctl.p_prompt.set_value(prompt)
|
||||
tool_ctl.p_idx.reposition(idx_x, 4)
|
||||
tool_ctl.p_idx.enable()
|
||||
tool_ctl.p_idx.set_max(idx_max)
|
||||
tool_ctl.p_idx.show()
|
||||
tool_ctl.p_unit.reposition(unit_x, unit_y)
|
||||
tool_ctl.p_unit.enable()
|
||||
tool_ctl.p_unit.show()
|
||||
tool_ctl.p_desc.reposition(1, desc_y)
|
||||
end
|
||||
|
||||
if type == "boilerValve" then
|
||||
tool_ctl.p_prompt.set_value("This is the # boiler for reactor unit # .")
|
||||
tool_ctl.p_idx.show()
|
||||
tool_ctl.p_idx.redraw()
|
||||
tool_ctl.p_idx.enable()
|
||||
tool_ctl.p_idx.set_max(2)
|
||||
tool_ctl.p_unit.reposition(44, 4)
|
||||
tool_ctl.p_unit.enable()
|
||||
tool_ctl.p_unit.show()
|
||||
reposition("This is reactor unit # 's # boiler.", 31, 2, 23, 4, 7)
|
||||
tool_ctl.p_assign_btn.hide(true)
|
||||
tool_ctl.p_assign_end.hide(true)
|
||||
tool_ctl.p_desc.reposition(1, 7)
|
||||
tool_ctl.p_desc.set_value("Each unit can have at most 2 boilers. Boiler #1 shows up first on the main display, followed by boiler #2 below it. These numberings are independent of which RTU they are connected to. For example, one RTU can have boiler #1 and another can have #2, but both cannot have #1.")
|
||||
tool_ctl.p_desc.set_value("Each unit can have at most 2 boilers. Boiler #1 shows up first on the main display, followed by boiler #2 below it. The numberings are per unit (unit 1 and unit 2 would both have a boiler #1 if each had one boiler) and can be split amongst multiple RTUs (one has #1, another has #2).")
|
||||
elseif type == "turbineValve" then
|
||||
tool_ctl.p_prompt.set_value("This is the # turbine for reactor unit # .")
|
||||
tool_ctl.p_idx.show()
|
||||
tool_ctl.p_idx.redraw()
|
||||
tool_ctl.p_idx.enable()
|
||||
tool_ctl.p_idx.set_max(3)
|
||||
tool_ctl.p_unit.reposition(45, 4)
|
||||
tool_ctl.p_unit.enable()
|
||||
tool_ctl.p_unit.show()
|
||||
reposition("This is reactor unit # 's # turbine.", 31, 3, 23, 4, 7)
|
||||
tool_ctl.p_assign_btn.hide(true)
|
||||
tool_ctl.p_assign_end.hide(true)
|
||||
tool_ctl.p_desc.reposition(1, 7)
|
||||
tool_ctl.p_desc.set_value("Each unit can have at most 3 turbines. Turbine #1 shows up first on the main display, followed by #2 then #3 below it. These numberings are independent of which RTU they are connected to. For example, one RTU can have turbine #1 and another can have #2, but both cannot have #1.")
|
||||
tool_ctl.p_desc.set_value("Each unit can have at most 3 turbines. Turbine #1 shows up first on the main display, followed by #2 then #3 below it. The numberings are per unit (unit 1 and unit 2 would both have a turbine #1) and can be split amongst multiple RTUs (one has #1, another has #2).")
|
||||
elseif type == "solarNeutronActivator" then
|
||||
reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7)
|
||||
tool_ctl.p_idx.hide()
|
||||
tool_ctl.p_prompt.set_value("This SNA is for reactor unit # .")
|
||||
tool_ctl.p_unit.reposition(31, 4)
|
||||
tool_ctl.p_unit.enable()
|
||||
tool_ctl.p_unit.show()
|
||||
tool_ctl.p_assign_btn.hide(true)
|
||||
tool_ctl.p_assign_end.hide(true)
|
||||
tool_ctl.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.")
|
||||
elseif type == "dynamicValve" then
|
||||
tool_ctl.p_prompt.set_value("This is the # dynamic tank for...")
|
||||
reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8)
|
||||
tool_ctl.p_assign_btn.show()
|
||||
tool_ctl.p_assign_btn.redraw()
|
||||
tool_ctl.p_assign_end.show()
|
||||
tool_ctl.p_assign_end.redraw()
|
||||
tool_ctl.p_idx.show()
|
||||
tool_ctl.p_idx.redraw()
|
||||
tool_ctl.p_idx.set_max(4)
|
||||
tool_ctl.p_unit.reposition(18, 6)
|
||||
tool_ctl.p_unit.enable()
|
||||
tool_ctl.p_unit.show()
|
||||
|
||||
if tool_ctl.p_assign_btn.get_value() == 1 then
|
||||
tool_ctl.p_idx.enable()
|
||||
@@ -885,22 +870,12 @@ local function config_view(display)
|
||||
tool_ctl.p_unit.enable()
|
||||
end
|
||||
|
||||
tool_ctl.p_desc.reposition(1, 8)
|
||||
tool_ctl.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.")
|
||||
elseif type == "environmentDetector" then
|
||||
tool_ctl.p_prompt.set_value("This is the # environment detector for...")
|
||||
reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8)
|
||||
tool_ctl.p_assign_btn.show()
|
||||
tool_ctl.p_assign_btn.redraw()
|
||||
tool_ctl.p_assign_end.show()
|
||||
tool_ctl.p_assign_end.redraw()
|
||||
tool_ctl.p_idx.show()
|
||||
tool_ctl.p_idx.redraw()
|
||||
tool_ctl.p_idx.set_max(99)
|
||||
tool_ctl.p_unit.reposition(18, 6)
|
||||
tool_ctl.p_unit.enable()
|
||||
tool_ctl.p_unit.show()
|
||||
if tool_ctl.p_assign_btn.get_value() == 1 then tool_ctl.p_unit.disable() else tool_ctl.p_unit.enable() end
|
||||
tool_ctl.p_desc.reposition(1, 8)
|
||||
tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.")
|
||||
elseif type == "inductionPort" or type == "spsPort" then
|
||||
local dev = tri(type == "inductionPort", "induction matrix", "SPS")
|
||||
@@ -908,7 +883,6 @@ local function config_view(display)
|
||||
tool_ctl.p_unit.hide(true)
|
||||
tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.")
|
||||
tool_ctl.p_assign_btn.hide(true)
|
||||
tool_ctl.p_assign_end.hide(true)
|
||||
tool_ctl.p_desc.reposition(1, 7)
|
||||
tool_ctl.p_desc.set_value("There can only be one of these devices per SCADA network, so it will be assigned as the sole " .. dev .. " for the facility. There must only be one of these across all the RTUs you have.")
|
||||
else
|
||||
@@ -965,11 +939,10 @@ local function config_view(display)
|
||||
|
||||
tool_ctl.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""}
|
||||
tool_ctl.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""}
|
||||
tool_ctl.p_idx = NumberField{parent=peri_c_4,x=14,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
tool_ctl.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility.","a unit. (unit #"},callback=function(v)tool_ctl.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
|
||||
tool_ctl.p_assign_end = TextBox{parent=peri_c_4,x=22,y=6,height=6,width=1,text=")"}
|
||||
tool_ctl.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=cpair(colors.lightGray,colors.white)}
|
||||
tool_ctl.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)tool_ctl.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
|
||||
|
||||
tool_ctl.p_unit = NumberField{parent=peri_c_4,x=44,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
tool_ctl.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=cpair(colors.lightGray,colors.white)}
|
||||
tool_ctl.p_unit.disable()
|
||||
|
||||
function tool_ctl.p_assign(opt)
|
||||
@@ -1484,7 +1457,7 @@ local function config_view(display)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
@@ -109,8 +109,8 @@ local function init(panel, units)
|
||||
--
|
||||
|
||||
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,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)
|
||||
|
||||
@@ -378,6 +378,7 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
-- send establish request (includes advertisement)
|
||||
---@param units table
|
||||
function public.send_establish(units)
|
||||
self.r_seq_num = nil
|
||||
_send(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) })
|
||||
end
|
||||
|
||||
@@ -444,7 +445,7 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
|
||||
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
elseif rtu_state.linked and (src_addr ~= self.sv_addr) then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..
|
||||
@@ -571,7 +572,9 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
end
|
||||
end
|
||||
|
||||
public.unlink(rtu_state)
|
||||
-- unlink
|
||||
self.sv_addr = comms.BROADCAST
|
||||
rtu_state.linked = false
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
|
||||
@@ -31,7 +31,7 @@ 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.10.1"
|
||||
local RTU_VERSION = "v1.10.8"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
|
||||
@@ -18,7 +18,7 @@ local comms = {}
|
||||
|
||||
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
|
||||
comms.version = "3.0.0"
|
||||
comms.api_version = "0.0.3"
|
||||
comms.api_version = "0.0.4"
|
||||
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
---@class types
|
||||
local types = {}
|
||||
|
||||
-- CLASSES --
|
||||
--#region CLASSES
|
||||
|
||||
---@class tank_fluid
|
||||
---@field name fluid
|
||||
@@ -67,12 +67,13 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
|
||||
---@field reactor integer
|
||||
---@field rsio table|nil
|
||||
|
||||
--#endregion
|
||||
|
||||
-- ALIASES --
|
||||
|
||||
---@alias color integer
|
||||
|
||||
-- ENUMERATION TYPES --
|
||||
--#region
|
||||
--#region ENUMERATION TYPES
|
||||
|
||||
---@enum TEMP_SCALE
|
||||
types.TEMP_SCALE = {
|
||||
@@ -96,6 +97,25 @@ types.TEMP_SCALE_UNITS = {
|
||||
"\xb0R"
|
||||
}
|
||||
|
||||
---@enum ENERGY_SCALE
|
||||
types.ENERGY_SCALE = {
|
||||
JOULES = 1,
|
||||
FE = 2,
|
||||
RF = 3
|
||||
}
|
||||
|
||||
types.ENERGY_SCALE_NAMES = {
|
||||
"Joules (J)",
|
||||
"Forge Energy (FE)",
|
||||
"Redstone Flux (RF)"
|
||||
}
|
||||
|
||||
types.ENERGY_SCALE_UNITS = {
|
||||
"J",
|
||||
"FE",
|
||||
"RF"
|
||||
}
|
||||
|
||||
---@enum PANEL_LINK_STATE
|
||||
types.PANEL_LINK_STATE = {
|
||||
LINKED = 1,
|
||||
@@ -150,6 +170,15 @@ function types.rtu_type_to_string(utype)
|
||||
end
|
||||
end
|
||||
|
||||
---@enum RTU_ID_FAIL
|
||||
types.RTU_ID_FAIL = {
|
||||
OK = 0,
|
||||
OUT_OF_RANGE = 1,
|
||||
DUPLICATE = 2,
|
||||
MAX_DEVICES = 3,
|
||||
MISSING = 4
|
||||
}
|
||||
|
||||
---@enum TRI_FAIL
|
||||
types.TRI_FAIL = {
|
||||
OK = 1,
|
||||
@@ -180,6 +209,23 @@ types.PROCESS_NAMES = {
|
||||
"GEN_RATE_FAULT_IDLE"
|
||||
}
|
||||
|
||||
---@enum AUTO_GROUP
|
||||
types.AUTO_GROUP = {
|
||||
MANUAL = 0,
|
||||
PRIMARY = 1,
|
||||
SECONDARY = 2,
|
||||
TERTIARY = 3,
|
||||
BACKUP = 4
|
||||
}
|
||||
|
||||
types.AUTO_GROUP_NAMES = {
|
||||
"Manual",
|
||||
"Primary",
|
||||
"Secondary",
|
||||
"Tertiary",
|
||||
"Backup"
|
||||
}
|
||||
|
||||
---@enum WASTE_MODE
|
||||
types.WASTE_MODE = {
|
||||
AUTO = 1,
|
||||
@@ -271,8 +317,7 @@ types.ALARM_STATE_NAMES = {
|
||||
|
||||
--#endregion
|
||||
|
||||
-- STRING TYPES --
|
||||
--#region
|
||||
--#region STRING TYPES
|
||||
|
||||
---@alias side
|
||||
---|"top"
|
||||
@@ -386,8 +431,7 @@ types.DUMPING_MODE = {
|
||||
|
||||
--#endregion
|
||||
|
||||
-- MODBUS --
|
||||
--#region
|
||||
--#region MODBUS
|
||||
|
||||
-- MODBUS function codes
|
||||
---@enum MODBUS_FCODE
|
||||
|
||||
@@ -24,7 +24,7 @@ local t_pack = table.pack
|
||||
local util = {}
|
||||
|
||||
-- scada-common version
|
||||
util.version = "1.4.0"
|
||||
util.version = "1.4.4"
|
||||
|
||||
util.TICK_TIME_S = 0.05
|
||||
util.TICK_TIME_MS = 50
|
||||
@@ -110,12 +110,31 @@ function util.pad(str, n)
|
||||
return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
|
||||
end
|
||||
|
||||
-- trim leading and trailing whitespace
|
||||
---@nodiscard
|
||||
---@param s string text
|
||||
---@return string
|
||||
function util.trim(s)
|
||||
local str = s:gsub("^%s*(.-)%s*$", "%1")
|
||||
return str
|
||||
end
|
||||
|
||||
-- wrap a string into a table of lines
|
||||
---@nodiscard
|
||||
---@param str string
|
||||
---@param limit integer line limit
|
||||
---@param limit integer line limit, must be greater than 0
|
||||
---@return table lines
|
||||
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
|
||||
function util.strwrap(str, limit)
|
||||
assert(limit > 0, "util.strwrap() limit not greater than 0")
|
||||
return cc_strings.wrap(str, limit)
|
||||
end
|
||||
|
||||
-- make sure a string is at least 'width' long
|
||||
---@nodiscard
|
||||
---@param str string
|
||||
---@param width integer minimum width
|
||||
---@return string string
|
||||
function util.strminw(str, width) return cc_strings.ensure_width(str, width) end
|
||||
|
||||
-- concatenation with built-in to string
|
||||
---@nodiscard
|
||||
@@ -372,65 +391,63 @@ end
|
||||
|
||||
--#region MEKANISM MATH
|
||||
|
||||
-- convert Joules to FE
|
||||
-- convert Joules to FE (or RF)
|
||||
---@nodiscard
|
||||
---@param J number Joules
|
||||
---@return number FE Forge Energy
|
||||
function util.joules_to_fe(J) return (J * 0.4) end
|
||||
---@return number FE Forge Energy or Redstone Flux
|
||||
function util.joules_to_fe_rf(J) return (J * 0.4) end
|
||||
|
||||
-- convert FE to Joules
|
||||
-- convert FE (or RF) to Joules
|
||||
---@nodiscard
|
||||
---@param FE number Forge Energy
|
||||
---@param FE number Forge Energy or Redstone Flux
|
||||
---@return number J Joules
|
||||
function util.fe_to_joules(FE) return (FE * 2.5) end
|
||||
function util.fe_rf_to_joules(FE) return (FE * 2.5) end
|
||||
|
||||
local function kFE(fe) return fe / 1000.0 end
|
||||
local function MFE(fe) return fe / 1000000.0 end
|
||||
local function GFE(fe) return fe / 1000000000.0 end
|
||||
local function TFE(fe) return fe / 1000000000000.0 end
|
||||
local function PFE(fe) return fe / 1000000000000000.0 end
|
||||
local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass
|
||||
local function ZFE(fe) return fe / 1000000000000000000000.0 end -- how & why did you do this?
|
||||
|
||||
-- format a power value into XXX.XX UNIT format (FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE)
|
||||
-- format a power value into XXX.XX UNIT format<br>
|
||||
-- example for FE: FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE
|
||||
---@nodiscard
|
||||
---@param fe number forge energy value
|
||||
---@param e number energy value
|
||||
---@param label string energy scale label
|
||||
---@param combine_label? boolean if a label should be included in the string itself
|
||||
---@param format? string format override
|
||||
---@return string str, string? unit
|
||||
function util.power_format(fe, combine_label, format)
|
||||
---@return string str, string unit
|
||||
function util.power_format(e, label, combine_label, format)
|
||||
local unit, value
|
||||
|
||||
if type(format) ~= "string" then format = "%.2f" end
|
||||
|
||||
if fe < 1000.0 then
|
||||
unit = "FE"
|
||||
value = fe
|
||||
elseif fe < 1000000.0 then
|
||||
unit = "kFE"
|
||||
value = kFE(fe)
|
||||
elseif fe < 1000000000.0 then
|
||||
unit = "MFE"
|
||||
value = MFE(fe)
|
||||
elseif fe < 1000000000000.0 then
|
||||
unit = "GFE"
|
||||
value = GFE(fe)
|
||||
elseif fe < 1000000000000000.0 then
|
||||
unit = "TFE"
|
||||
value = TFE(fe)
|
||||
elseif fe < 1000000000000000000.0 then
|
||||
unit = "PFE"
|
||||
value = PFE(fe)
|
||||
elseif fe < 1000000000000000000000.0 then
|
||||
unit = "EFE"
|
||||
value = EFE(fe)
|
||||
if e < 1000.0 then
|
||||
unit = ""
|
||||
value = e
|
||||
elseif e < 1000000.0 then
|
||||
unit = "k"
|
||||
value = e / 1000.0
|
||||
elseif e < 1000000000.0 then
|
||||
unit = "M"
|
||||
value = e / 1000000.0
|
||||
elseif e < 1000000000000.0 then
|
||||
unit = "G"
|
||||
value = e / 1000000000.0
|
||||
elseif e < 1000000000000000.0 then
|
||||
unit = "T"
|
||||
value = e / 1000000000000.0
|
||||
elseif e < 1000000000000000000.0 then
|
||||
unit = "P"
|
||||
value = e / 1000000000000000.0
|
||||
elseif e < 1000000000000000000000.0 then
|
||||
-- if you accomplish this please touch grass
|
||||
unit = "E"
|
||||
value = e / 1000000000000000000.0
|
||||
else
|
||||
unit = "ZFE"
|
||||
value = ZFE(fe)
|
||||
-- how & why did you do this?
|
||||
unit = "Z"
|
||||
value = e / 1000000000000000000000.0
|
||||
end
|
||||
|
||||
unit = unit .. label
|
||||
|
||||
if combine_label then
|
||||
return util.sprintf(util.c(format, " %s"), value, unit)
|
||||
return util.sprintf(util.c(format, " %s"), value, unit), unit
|
||||
else
|
||||
return util.sprintf(format, value), unit
|
||||
end
|
||||
|
||||
@@ -30,7 +30,6 @@ local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
@@ -181,7 +180,7 @@ local function config_view(display)
|
||||
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Supervisor configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device had no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 5
|
||||
end
|
||||
|
||||
@@ -453,15 +452,16 @@ local function config_view(display)
|
||||
|
||||
vis_unit_list.set_value(u_text)
|
||||
|
||||
local vis_ftanks = tool_ctl.vis_ftanks
|
||||
local next_idx = 1
|
||||
|
||||
if is_ft(1) then
|
||||
next_idx = 2
|
||||
|
||||
if (mode == 1 and (is_ft(2) or is_ft(3) or is_ft(4))) or (mode == 2 and (is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 5) and is_ft(2)) then
|
||||
tool_ctl.vis_ftanks[1].pipe_direct.set_value("\x8c\x8c\x8c\x9c\x8c")
|
||||
vis_ftanks[1].pipe_direct.set_value("\x8c\x8c\x8c\x9c\x8c")
|
||||
else
|
||||
tool_ctl.vis_ftanks[1].pipe_direct.set_value(string.rep("\x8c",5))
|
||||
vis_ftanks[1].pipe_direct.set_value(string.rep("\x8c",5))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -469,108 +469,108 @@ local function config_view(display)
|
||||
local _2_46_need_chain = (mode == 4 and (is_ft(3) or is_ft(4))) or (mode == 6 and is_ft(3))
|
||||
|
||||
if is_ft(2) then
|
||||
tool_ctl.vis_ftanks[2].label.set_value("Tank F" .. next_idx)
|
||||
vis_ftanks[2].label.set_value("Tank F" .. next_idx)
|
||||
|
||||
if (mode < 4 or mode == 5) and is_ft(1) then
|
||||
tool_ctl.vis_ftanks[2].label.hide(true)
|
||||
tool_ctl.vis_ftanks[2].pipe_direct.hide(true)
|
||||
vis_ftanks[2].label.hide(true)
|
||||
vis_ftanks[2].pipe_direct.hide(true)
|
||||
if _2_12_need_passt then
|
||||
tool_ctl.vis_ftanks[2].pipe_chain.set_value("\x95\n\x9d")
|
||||
vis_ftanks[2].pipe_chain.set_value("\x95\n\x9d")
|
||||
else
|
||||
tool_ctl.vis_ftanks[2].pipe_chain.set_value("\x95\n\x8d")
|
||||
vis_ftanks[2].pipe_chain.set_value("\x95\n\x8d")
|
||||
end
|
||||
tool_ctl.vis_ftanks[2].pipe_chain.show()
|
||||
vis_ftanks[2].pipe_chain.show()
|
||||
else
|
||||
tool_ctl.vis_ftanks[2].label.show()
|
||||
vis_ftanks[2].label.show()
|
||||
next_idx = next_idx + 1
|
||||
|
||||
tool_ctl.vis_ftanks[2].pipe_chain.hide(true)
|
||||
vis_ftanks[2].pipe_chain.hide(true)
|
||||
if _2_12_need_passt or _2_46_need_chain then
|
||||
tool_ctl.vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
|
||||
vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
|
||||
else
|
||||
tool_ctl.vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
|
||||
vis_ftanks[2].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
|
||||
end
|
||||
tool_ctl.vis_ftanks[2].pipe_direct.show()
|
||||
vis_ftanks[2].pipe_direct.show()
|
||||
end
|
||||
|
||||
tool_ctl.vis_ftanks[2].line.show()
|
||||
vis_ftanks[2].line.show()
|
||||
elseif is_ft(1) and _2_12_need_passt then
|
||||
tool_ctl.vis_ftanks[2].label.hide(true)
|
||||
tool_ctl.vis_ftanks[2].pipe_direct.hide(true)
|
||||
tool_ctl.vis_ftanks[2].pipe_chain.set_value("\x95\n\x95")
|
||||
tool_ctl.vis_ftanks[2].pipe_chain.show()
|
||||
tool_ctl.vis_ftanks[2].line.show()
|
||||
vis_ftanks[2].label.hide(true)
|
||||
vis_ftanks[2].pipe_direct.hide(true)
|
||||
vis_ftanks[2].pipe_chain.set_value("\x95\n\x95")
|
||||
vis_ftanks[2].pipe_chain.show()
|
||||
vis_ftanks[2].line.show()
|
||||
else
|
||||
tool_ctl.vis_ftanks[2].line.hide(true)
|
||||
vis_ftanks[2].line.hide(true)
|
||||
end
|
||||
|
||||
if is_ft(3) then
|
||||
tool_ctl.vis_ftanks[3].label.set_value("Tank F" .. next_idx)
|
||||
vis_ftanks[3].label.set_value("Tank F" .. next_idx)
|
||||
|
||||
if (mode < 3 and (is_ft(1) or is_ft(2))) or ((mode == 4 or mode == 6) and is_ft(2)) then
|
||||
tool_ctl.vis_ftanks[3].label.hide(true)
|
||||
tool_ctl.vis_ftanks[3].pipe_direct.hide(true)
|
||||
vis_ftanks[3].label.hide(true)
|
||||
vis_ftanks[3].pipe_direct.hide(true)
|
||||
if (mode == 1 or mode == 4) and is_ft(4) then
|
||||
tool_ctl.vis_ftanks[3].pipe_chain.set_value("\x95\n\x9d")
|
||||
vis_ftanks[3].pipe_chain.set_value("\x95\n\x9d")
|
||||
else
|
||||
tool_ctl.vis_ftanks[3].pipe_chain.set_value("\x95\n\x8d")
|
||||
vis_ftanks[3].pipe_chain.set_value("\x95\n\x8d")
|
||||
end
|
||||
tool_ctl.vis_ftanks[3].pipe_chain.show()
|
||||
vis_ftanks[3].pipe_chain.show()
|
||||
else
|
||||
tool_ctl.vis_ftanks[3].label.show()
|
||||
vis_ftanks[3].label.show()
|
||||
next_idx = next_idx + 1
|
||||
|
||||
tool_ctl.vis_ftanks[3].pipe_chain.hide(true)
|
||||
vis_ftanks[3].pipe_chain.hide(true)
|
||||
if (mode == 1 or mode == 3 or mode == 4 or mode == 7) and is_ft(4) then
|
||||
tool_ctl.vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
|
||||
vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x9c")
|
||||
else
|
||||
tool_ctl.vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
|
||||
vis_ftanks[3].pipe_direct.set_value("\x8c\x8c\x8c\x8c")
|
||||
end
|
||||
tool_ctl.vis_ftanks[3].pipe_direct.show()
|
||||
vis_ftanks[3].pipe_direct.show()
|
||||
end
|
||||
|
||||
tool_ctl.vis_ftanks[3].line.show()
|
||||
vis_ftanks[3].line.show()
|
||||
elseif (mode == 1 and is_ft(4) and (is_ft(1) or is_ft(2))) or (mode == 4 and is_ft(2) and is_ft(4)) then
|
||||
tool_ctl.vis_ftanks[3].label.hide(true)
|
||||
tool_ctl.vis_ftanks[3].pipe_direct.hide(true)
|
||||
tool_ctl.vis_ftanks[3].pipe_chain.set_value("\x95\n\x95")
|
||||
tool_ctl.vis_ftanks[3].pipe_chain.show()
|
||||
tool_ctl.vis_ftanks[3].line.show()
|
||||
vis_ftanks[3].label.hide(true)
|
||||
vis_ftanks[3].pipe_direct.hide(true)
|
||||
vis_ftanks[3].pipe_chain.set_value("\x95\n\x95")
|
||||
vis_ftanks[3].pipe_chain.show()
|
||||
vis_ftanks[3].line.show()
|
||||
else
|
||||
tool_ctl.vis_ftanks[3].line.hide(true)
|
||||
vis_ftanks[3].line.hide(true)
|
||||
end
|
||||
|
||||
if is_ft(4) then
|
||||
tool_ctl.vis_ftanks[4].label.set_value("Tank F" .. next_idx)
|
||||
vis_ftanks[4].label.set_value("Tank F" .. next_idx)
|
||||
|
||||
if (mode == 1 and (is_ft(1) or is_ft(2) or is_ft(3))) or ((mode == 3 or mode == 7) and is_ft(3)) or (mode == 4 and (is_ft(2) or is_ft(3))) then
|
||||
tool_ctl.vis_ftanks[4].label.hide(true)
|
||||
tool_ctl.vis_ftanks[4].pipe_direct.hide(true)
|
||||
tool_ctl.vis_ftanks[4].pipe_chain.show()
|
||||
vis_ftanks[4].label.hide(true)
|
||||
vis_ftanks[4].pipe_direct.hide(true)
|
||||
vis_ftanks[4].pipe_chain.show()
|
||||
else
|
||||
tool_ctl.vis_ftanks[4].label.show()
|
||||
tool_ctl.vis_ftanks[4].pipe_chain.hide(true)
|
||||
tool_ctl.vis_ftanks[4].pipe_direct.show()
|
||||
vis_ftanks[4].label.show()
|
||||
vis_ftanks[4].pipe_chain.hide(true)
|
||||
vis_ftanks[4].pipe_direct.show()
|
||||
end
|
||||
|
||||
tool_ctl.vis_ftanks[4].line.show()
|
||||
vis_ftanks[4].line.show()
|
||||
else
|
||||
tool_ctl.vis_ftanks[4].line.hide(true)
|
||||
vis_ftanks[4].line.hide(true)
|
||||
end
|
||||
end
|
||||
|
||||
local function change_mode(mode)
|
||||
tmp_cfg.FacilityTankMode = mode
|
||||
tool_ctl.vis_draw(mode)
|
||||
end
|
||||
|
||||
local tank_modes = { "Mode 1", "Mode 2", "Mode 3", "Mode 4", "Mode 5", "Mode 6", "Mode 7", "Mode 8" }
|
||||
local tank_mode = RadioButton{parent=svr_c_5,x=1,y=4,callback=tool_ctl.vis_draw,default=math.max(1,ini_cfg.FacilityTankMode),options=tank_modes,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow}
|
||||
local tank_mode = RadioButton{parent=svr_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}
|
||||
|
||||
--#endregion
|
||||
|
||||
local function submit_mode()
|
||||
tmp_cfg.FacilityTankMode = tank_mode.get_value()
|
||||
svr_pane.set_value(7)
|
||||
end
|
||||
|
||||
PushButton{parent=svr_c_5,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=svr_c_5,x=44,y=14,text="Next \x1a",callback=submit_mode,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=svr_c_5,x=44,y=14,text="Next \x1a",callback=function()svr_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
PushButton{parent=svr_c_5,x=8,y=14,min_width=7,text="About",callback=function()svr_pane.set_value(6)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
@@ -906,7 +906,10 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
for _, field in ipairs(fields) do
|
||||
local k, v = field[1], tmp_cfg[field[1]]
|
||||
if v == nil then settings.unset(k) else settings.set(k, v) end
|
||||
end
|
||||
|
||||
if settings.save("/supervisor.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
@@ -1170,7 +1173,7 @@ local function config_view(display)
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
832
supervisor/facility_update.lua
Normal file
832
supervisor/facility_update.lua
Normal file
@@ -0,0 +1,832 @@
|
||||
local audio = require("scada-common.audio")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local qtypes = require("supervisor.session.rtu.qtypes")
|
||||
|
||||
local TONE = audio.TONE
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
local PROCESS = types.PROCESS
|
||||
local PROCESS_NAMES = types.PROCESS_NAMES
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
|
||||
local IO = rsio.IO
|
||||
|
||||
local ALARM_LIMS = const.ALARM_LIMITS
|
||||
|
||||
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
|
||||
|
||||
-- 7.14 kJ per blade for 1 mB of fissile fuel<br>
|
||||
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
|
||||
local POWER_PER_BLADE = util.joules_to_fe_rf(7140)
|
||||
|
||||
local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000
|
||||
|
||||
local CHARGE_Kp = 0.15
|
||||
local CHARGE_Ki = 0.0
|
||||
local CHARGE_Kd = 0.6
|
||||
|
||||
local RATE_Kp = 2.45
|
||||
local RATE_Ki = 0.4825
|
||||
local RATE_Kd = -1.0
|
||||
|
||||
local self = nil ---@type _facility_self
|
||||
local next_mode = 0
|
||||
local charge_update = 0
|
||||
local rate_update = 0
|
||||
|
||||
---@class facility_update_extension
|
||||
local update = {}
|
||||
|
||||
--#region PRIVATE FUNCTIONS
|
||||
|
||||
-- check if all auto-controlled units completed ramping
|
||||
---@nodiscard
|
||||
local function all_units_ramped()
|
||||
local all_ramped = true
|
||||
|
||||
for i = 1, #self.prio_defs do
|
||||
local units = self.prio_defs[i]
|
||||
for u = 1, #units do
|
||||
all_ramped = all_ramped and units[u].auto_ramp_complete()
|
||||
end
|
||||
end
|
||||
|
||||
return all_ramped
|
||||
end
|
||||
|
||||
-- split a burn rate among the reactors
|
||||
---@param burn_rate number burn rate assignment
|
||||
---@param ramp boolean true to ramp, false to set right away
|
||||
---@param abort_on_fault boolean? true to exit if one device has an effective burn rate different than its limit
|
||||
---@return integer unallocated_br100, boolean? aborted
|
||||
local function allocate_burn_rate(burn_rate, ramp, abort_on_fault)
|
||||
local unallocated = math.floor(burn_rate * 100)
|
||||
|
||||
-- go through all priority groups
|
||||
for i = 1, #self.prio_defs do
|
||||
local units = self.prio_defs[i]
|
||||
|
||||
if #units > 0 then
|
||||
local split = math.floor(unallocated / #units)
|
||||
|
||||
local splits = {}
|
||||
for u = 1, #units do splits[u] = split end
|
||||
splits[#units] = splits[#units] + (unallocated % #units)
|
||||
|
||||
-- go through all reactor units in this group
|
||||
for id = 1, #units do
|
||||
local u = units[id] ---@type reactor_unit
|
||||
|
||||
local ctl = u.get_control_inf()
|
||||
local lim_br100 = u.auto_get_effective_limit()
|
||||
|
||||
if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then
|
||||
-- effective limit differs from set limit, unit is degraded
|
||||
return unallocated, true
|
||||
end
|
||||
|
||||
local last = ctl.br100
|
||||
|
||||
if splits[id] <= lim_br100 then
|
||||
ctl.br100 = splits[id]
|
||||
else
|
||||
ctl.br100 = lim_br100
|
||||
|
||||
if id < #units then
|
||||
local remaining = #units - id
|
||||
split = math.floor(unallocated / remaining)
|
||||
for x = (id + 1), #units do splits[x] = split end
|
||||
splits[#units] = splits[#units] + (unallocated % remaining)
|
||||
end
|
||||
end
|
||||
|
||||
unallocated = math.max(0, unallocated - ctl.br100)
|
||||
|
||||
if last ~= ctl.br100 then u.auto_commit_br100(ramp) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return unallocated, false
|
||||
end
|
||||
|
||||
-- set idle state of all assigned reactors
|
||||
---@param idle boolean idle state
|
||||
local function set_idling(idle)
|
||||
for i = 1, #self.prio_defs do
|
||||
for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region PUBLIC FUNCTIONS
|
||||
|
||||
-- automatic control pre-update logic
|
||||
function update.pre_auto()
|
||||
-- unlink RTU sessions if they are closed
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
||||
|
||||
-- check if test routines are allowed right now
|
||||
self.allow_testing = true
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
self.allow_testing = self.allow_testing and u.is_safe_idle()
|
||||
end
|
||||
|
||||
-- current state for process control
|
||||
charge_update = 0
|
||||
rate_update = 0
|
||||
|
||||
-- calculate moving averages for induction matrix
|
||||
if self.induction[1] ~= nil then
|
||||
local matrix = self.induction[1] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
|
||||
local build_update = db.build.last_update
|
||||
rate_update = db.state.last_update
|
||||
charge_update = db.tanks.last_update
|
||||
|
||||
local has_data = build_update > 0 and rate_update > 0 and charge_update > 0
|
||||
|
||||
if matrix.is_faulted() then
|
||||
-- a fault occured, cannot reliably update stats
|
||||
has_data = false
|
||||
self.im_stat_init = false
|
||||
self.imtx_faulted_times = { build_update, rate_update, charge_update }
|
||||
elseif not self.im_stat_init then
|
||||
-- prevent operation with partially invalid data
|
||||
-- all fields must have updated since the last fault
|
||||
has_data = self.imtx_faulted_times[1] < build_update and
|
||||
self.imtx_faulted_times[2] < rate_update and
|
||||
self.imtx_faulted_times[3] < charge_update
|
||||
end
|
||||
|
||||
if has_data then
|
||||
local energy = util.joules_to_fe_rf(db.tanks.energy)
|
||||
local input = util.joules_to_fe_rf(db.state.last_input)
|
||||
local output = util.joules_to_fe_rf(db.state.last_output)
|
||||
|
||||
if self.im_stat_init then
|
||||
self.avg_charge.record(energy, charge_update)
|
||||
self.avg_inflow.record(input, rate_update)
|
||||
self.avg_outflow.record(output, rate_update)
|
||||
|
||||
if charge_update ~= self.imtx_last_charge_t then
|
||||
local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t)
|
||||
|
||||
self.imtx_last_charge = energy
|
||||
self.imtx_last_charge_t = charge_update
|
||||
|
||||
-- if the capacity changed, toss out existing data
|
||||
if db.build.max_energy ~= self.imtx_last_capacity then
|
||||
self.imtx_last_capacity = db.build.max_energy
|
||||
self.avg_net.reset()
|
||||
else
|
||||
self.avg_net.record(delta, charge_update)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.im_stat_init = true
|
||||
|
||||
self.avg_charge.reset(energy)
|
||||
self.avg_inflow.reset(input)
|
||||
self.avg_outflow.reset(output)
|
||||
self.avg_net.reset()
|
||||
|
||||
self.imtx_last_capacity = db.build.max_energy
|
||||
self.imtx_last_charge = energy
|
||||
self.imtx_last_charge_t = charge_update
|
||||
end
|
||||
else
|
||||
-- prevent use by control systems
|
||||
rate_update = 0
|
||||
charge_update = 0
|
||||
end
|
||||
else
|
||||
self.im_stat_init = false
|
||||
end
|
||||
|
||||
self.all_sys_ok = true
|
||||
for i = 1, #self.units do
|
||||
self.all_sys_ok = self.all_sys_ok and not self.units[i].get_control_inf().degraded
|
||||
end
|
||||
end
|
||||
|
||||
-- run auto control
|
||||
---@param ExtChargeIdling boolean ExtChargeIdling config field
|
||||
function update.auto_control(ExtChargeIdling)
|
||||
local AUTO_SCRAM = self.types.AUTO_SCRAM
|
||||
local START_STATUS = self.types.START_STATUS
|
||||
|
||||
local avg_charge = self.avg_charge.compute()
|
||||
local avg_inflow = self.avg_inflow.compute()
|
||||
local avg_outflow = self.avg_outflow.compute()
|
||||
|
||||
local now = os.clock()
|
||||
|
||||
local state_changed = self.mode ~= self.last_mode
|
||||
next_mode = self.mode
|
||||
|
||||
-- once auto control is started, sort the priority sublists by limits
|
||||
if state_changed then
|
||||
self.saturated = false
|
||||
|
||||
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
|
||||
|
||||
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
|
||||
self.start_fail = START_STATUS.OK
|
||||
|
||||
if (self.mode ~= PROCESS.MATRIX_FAULT_IDLE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
|
||||
-- auto clear ASCRAM
|
||||
self.ascram = false
|
||||
self.ascram_reason = AUTO_SCRAM.NONE
|
||||
end
|
||||
|
||||
local blade_count = nil
|
||||
self.max_burn_combined = 0.0
|
||||
|
||||
for i = 1, #self.prio_defs do
|
||||
table.sort(self.prio_defs[i],
|
||||
---@param a reactor_unit
|
||||
---@param b reactor_unit
|
||||
function (a, b) return a.get_control_inf().lim_br100 < b.get_control_inf().lim_br100 end
|
||||
)
|
||||
|
||||
for _, u in pairs(self.prio_defs[i]) do
|
||||
local u_blade_count = u.get_control_inf().blade_count
|
||||
|
||||
if blade_count == nil then
|
||||
blade_count = u_blade_count
|
||||
elseif (u_blade_count ~= blade_count) and (self.mode == PROCESS.GEN_RATE) then
|
||||
log.warning("FAC: cannot start GEN_RATE process with inconsistent unit blade counts")
|
||||
next_mode = PROCESS.INACTIVE
|
||||
self.start_fail = START_STATUS.BLADE_MISMATCH
|
||||
end
|
||||
|
||||
if self.start_fail == START_STATUS.OK then u.auto_engage() end
|
||||
|
||||
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0)
|
||||
end
|
||||
end
|
||||
|
||||
log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t"))
|
||||
|
||||
if blade_count == nil then
|
||||
-- no units
|
||||
log.warning("FAC: cannot start process control with 0 units assigned")
|
||||
next_mode = PROCESS.INACTIVE
|
||||
self.start_fail = START_STATUS.NO_UNITS
|
||||
else
|
||||
self.charge_conversion = blade_count * POWER_PER_BLADE
|
||||
end
|
||||
elseif self.mode == PROCESS.INACTIVE then
|
||||
for i = 1, #self.prio_defs do
|
||||
-- disable reactors and disengage auto control
|
||||
for _, u in pairs(self.prio_defs[i]) do
|
||||
u.disable()
|
||||
u.auto_set_idle(false)
|
||||
u.auto_disengage()
|
||||
end
|
||||
end
|
||||
|
||||
log.info("FAC: disengaging auto control (now inactive)")
|
||||
end
|
||||
|
||||
self.initial_ramp = true
|
||||
self.waiting_on_ramp = false
|
||||
self.waiting_on_stable = false
|
||||
else
|
||||
self.initial_ramp = false
|
||||
end
|
||||
|
||||
-- update unit ready state
|
||||
local assign_count = 0
|
||||
self.units_ready = true
|
||||
for i = 1, #self.prio_defs do
|
||||
for _, u in pairs(self.prio_defs[i]) do
|
||||
assign_count = assign_count + 1
|
||||
self.units_ready = self.units_ready and u.get_control_inf().ready
|
||||
end
|
||||
end
|
||||
|
||||
-- perform mode-specific operations
|
||||
if self.mode == PROCESS.INACTIVE then
|
||||
if not self.units_ready then
|
||||
self.status_text = { "NOT READY", "assigned units not ready" }
|
||||
else
|
||||
-- clear ASCRAM once ready
|
||||
self.ascram = false
|
||||
self.ascram_reason = AUTO_SCRAM.NONE
|
||||
|
||||
if self.start_fail == START_STATUS.NO_UNITS and assign_count == 0 then
|
||||
self.status_text = { "START FAILED", "no units were assigned" }
|
||||
elseif self.start_fail == START_STATUS.BLADE_MISMATCH then
|
||||
self.status_text = { "START FAILED", "turbine blade count mismatch" }
|
||||
else
|
||||
self.status_text = { "IDLE", "control disengaged" }
|
||||
end
|
||||
end
|
||||
elseif self.mode == PROCESS.MAX_BURN then
|
||||
-- run units at their limits
|
||||
if state_changed then
|
||||
self.time_start = now
|
||||
self.saturated = true
|
||||
|
||||
self.status_text = { "MONITORED MODE", "running reactors at limit" }
|
||||
log.info("FAC: MAX_BURN process mode started")
|
||||
end
|
||||
|
||||
allocate_burn_rate(self.max_burn_combined, true)
|
||||
elseif self.mode == PROCESS.BURN_RATE then
|
||||
-- a total aggregate burn rate
|
||||
if state_changed then
|
||||
self.time_start = now
|
||||
self.status_text = { "BURN RATE MODE", "running" }
|
||||
log.info("FAC: BURN_RATE process mode started")
|
||||
end
|
||||
|
||||
local unallocated = allocate_burn_rate(self.burn_target, true)
|
||||
self.saturated = self.burn_target == self.max_burn_combined or unallocated > 0
|
||||
elseif self.mode == PROCESS.CHARGE then
|
||||
-- target a level of charge
|
||||
if state_changed then
|
||||
self.time_start = now
|
||||
self.last_time = now
|
||||
self.last_error = 0
|
||||
self.accumulator = 0
|
||||
|
||||
-- enabling idling on all assigned units
|
||||
set_idling(true)
|
||||
|
||||
self.status_text = { "CHARGE MODE", "running control loop" }
|
||||
log.info("FAC: CHARGE mode starting PID control")
|
||||
elseif self.last_update < charge_update then
|
||||
-- convert to kFE to make constants not microscopic
|
||||
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
|
||||
|
||||
-- stop accumulator when saturated to avoid windup
|
||||
if not self.saturated then
|
||||
self.accumulator = self.accumulator + (error * (now - self.last_time))
|
||||
end
|
||||
|
||||
-- local runtime = now - self.time_start
|
||||
local integral = self.accumulator
|
||||
local derivative = (error - self.last_error) / (now - self.last_time)
|
||||
|
||||
local P = CHARGE_Kp * error
|
||||
local I = CHARGE_Ki * integral
|
||||
local D = CHARGE_Kd * derivative
|
||||
|
||||
local output = P + I + D
|
||||
|
||||
-- clamp at range -> output clamped (out_c)
|
||||
local out_c = math.max(0, math.min(output, self.max_burn_combined))
|
||||
|
||||
self.saturated = output ~= out_c
|
||||
|
||||
if not ExtChargeIdling then
|
||||
-- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge
|
||||
set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0)))
|
||||
end
|
||||
|
||||
-- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
|
||||
-- runtime, avg_charge, error, integral, output, out_c, P, I, D))
|
||||
|
||||
allocate_burn_rate(out_c, true)
|
||||
|
||||
self.last_time = now
|
||||
self.last_error = error
|
||||
end
|
||||
|
||||
self.last_update = charge_update
|
||||
elseif self.mode == PROCESS.GEN_RATE then
|
||||
-- target a rate of generation
|
||||
if state_changed then
|
||||
-- estimate an initial output
|
||||
local output = self.gen_rate_setpoint / self.charge_conversion
|
||||
|
||||
local unallocated = allocate_burn_rate(output, true)
|
||||
|
||||
self.saturated = output >= self.max_burn_combined or unallocated > 0
|
||||
self.waiting_on_ramp = true
|
||||
|
||||
self.status_text = { "GENERATION MODE", "starting up" }
|
||||
log.info(util.c("FAC: GEN_RATE process mode initial ramp started (initial target is ", output, " mB/t)"))
|
||||
elseif self.waiting_on_ramp then
|
||||
if all_units_ramped() then
|
||||
self.waiting_on_ramp = false
|
||||
self.waiting_on_stable = true
|
||||
|
||||
self.time_start = now
|
||||
|
||||
self.status_text = { "GENERATION MODE", "holding ramped rate" }
|
||||
log.info("FAC: GEN_RATE process mode initial ramp completed, holding for stablization time")
|
||||
end
|
||||
elseif self.waiting_on_stable then
|
||||
if (now - self.time_start) > FLOW_STABILITY_DELAY_S then
|
||||
self.waiting_on_stable = false
|
||||
|
||||
self.time_start = now
|
||||
self.last_time = now
|
||||
self.last_error = 0
|
||||
self.accumulator = 0
|
||||
|
||||
self.status_text = { "GENERATION MODE", "running control loop" }
|
||||
log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control")
|
||||
end
|
||||
elseif self.last_update < rate_update then
|
||||
-- convert to MFE (in rounded kFE) to make constants not microscopic
|
||||
local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000
|
||||
|
||||
-- stop accumulator when saturated to avoid windup
|
||||
if not self.saturated then
|
||||
self.accumulator = self.accumulator + (error * (now - self.last_time))
|
||||
end
|
||||
|
||||
-- local runtime = now - self.time_start
|
||||
local integral = self.accumulator
|
||||
local derivative = (error - self.last_error) / (now - self.last_time)
|
||||
|
||||
local P = RATE_Kp * error
|
||||
local I = RATE_Ki * integral
|
||||
local D = RATE_Kd * derivative
|
||||
|
||||
-- velocity (rate) (derivative of charge level => rate) feed forward
|
||||
local FF = self.gen_rate_setpoint / self.charge_conversion
|
||||
|
||||
local output = P + I + D + FF
|
||||
|
||||
-- clamp at range -> output clamped (sp_c)
|
||||
local out_c = math.max(0, math.min(output, self.max_burn_combined))
|
||||
|
||||
self.saturated = output ~= out_c
|
||||
|
||||
-- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
|
||||
-- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
|
||||
|
||||
allocate_burn_rate(out_c, false)
|
||||
|
||||
self.last_time = now
|
||||
self.last_error = error
|
||||
end
|
||||
|
||||
self.last_update = rate_update
|
||||
elseif self.mode == PROCESS.MATRIX_FAULT_IDLE then
|
||||
-- exceeded charge, wait until condition clears
|
||||
if self.ascram_reason == AUTO_SCRAM.NONE then
|
||||
next_mode = self.return_mode
|
||||
log.info("FAC: exiting matrix fault idle state due to fault resolution")
|
||||
elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then
|
||||
next_mode = PROCESS.SYSTEM_ALARM_IDLE
|
||||
log.info("FAC: exiting matrix fault idle state due to critical unit alarm")
|
||||
end
|
||||
elseif self.mode == PROCESS.SYSTEM_ALARM_IDLE then
|
||||
-- do nothing, wait for user to confirm (stop and reset)
|
||||
elseif self.mode == PROCESS.GEN_RATE_FAULT_IDLE then
|
||||
-- system faulted (degraded/not ready) while running generation rate mode
|
||||
-- mode will need to be fully restarted once everything is OK to re-ramp to feed-forward
|
||||
if self.units_ready then
|
||||
log.info("FAC: system ready after faulting out of GEN_RATE process mode, switching back...")
|
||||
next_mode = PROCESS.GEN_RATE
|
||||
end
|
||||
elseif self.mode ~= PROCESS.INACTIVE then
|
||||
log.error(util.c("FAC: unsupported process mode ", self.mode, ", switching to inactive"))
|
||||
next_mode = PROCESS.INACTIVE
|
||||
end
|
||||
end
|
||||
|
||||
-- update automatic safety logic
|
||||
function update.auto_safety()
|
||||
local AUTO_SCRAM = self.types.AUTO_SCRAM
|
||||
|
||||
local astatus = self.ascram_status
|
||||
|
||||
if self.induction[1] ~= nil then
|
||||
local db = self.induction[1].get_db() ---@type imatrix_session_db
|
||||
|
||||
-- clear matrix disconnected
|
||||
if astatus.matrix_dc then
|
||||
astatus.matrix_dc = false
|
||||
log.info("FAC: induction matrix reconnected, clearing ASCRAM condition")
|
||||
end
|
||||
|
||||
-- check matrix fill too high
|
||||
local was_fill = astatus.matrix_fill
|
||||
astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
|
||||
|
||||
if was_fill and not astatus.matrix_fill then
|
||||
log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%"))
|
||||
end
|
||||
|
||||
-- check for critical unit alarms
|
||||
astatus.crit_alarm = false
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
|
||||
if u.has_alarm_min_prio(PRIO.CRITICAL) then
|
||||
astatus.crit_alarm = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- check for facility radiation
|
||||
if #self.envd > 0 then
|
||||
local max_rad = 0
|
||||
|
||||
for i = 1, #self.envd do
|
||||
local envd = self.envd[i] ---@type unit_session
|
||||
local e_db = envd.get_db() ---@type envd_session_db
|
||||
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
|
||||
end
|
||||
|
||||
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
|
||||
else
|
||||
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
|
||||
-- operator can restart the system or hit the stop/reset button
|
||||
end
|
||||
|
||||
-- system not ready, will need to restart GEN_RATE mode
|
||||
-- clears when we enter the fault waiting state
|
||||
astatus.gen_fault = self.mode == PROCESS.GEN_RATE and not self.units_ready
|
||||
else
|
||||
astatus.matrix_dc = true
|
||||
end
|
||||
|
||||
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
|
||||
local scram = astatus.matrix_dc or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault
|
||||
|
||||
if scram and not self.ascram then
|
||||
-- SCRAM all units
|
||||
for i = 1, #self.prio_defs do
|
||||
for _, u in pairs(self.prio_defs[i]) do
|
||||
u.auto_scram()
|
||||
end
|
||||
end
|
||||
|
||||
if astatus.crit_alarm then
|
||||
-- highest priority alarm
|
||||
next_mode = PROCESS.SYSTEM_ALARM_IDLE
|
||||
self.ascram_reason = AUTO_SCRAM.CRIT_ALARM
|
||||
self.status_text = { "AUTOMATIC SCRAM", "critical unit alarm tripped" }
|
||||
|
||||
log.info("FAC: automatic SCRAM due to critical unit alarm")
|
||||
log.warning("FAC: emergency exit of process control due to critical unit alarm")
|
||||
elseif astatus.radiation then
|
||||
next_mode = PROCESS.SYSTEM_ALARM_IDLE
|
||||
self.ascram_reason = AUTO_SCRAM.RADIATION
|
||||
self.status_text = { "AUTOMATIC SCRAM", "facility radiation high" }
|
||||
|
||||
log.info("FAC: automatic SCRAM due to high facility radiation")
|
||||
elseif astatus.matrix_dc then
|
||||
next_mode = PROCESS.MATRIX_FAULT_IDLE
|
||||
self.ascram_reason = AUTO_SCRAM.MATRIX_DC
|
||||
self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" }
|
||||
|
||||
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
|
||||
|
||||
log.info("FAC: automatic SCRAM due to induction matrix disconnection")
|
||||
elseif astatus.matrix_fill then
|
||||
next_mode = PROCESS.MATRIX_FAULT_IDLE
|
||||
self.ascram_reason = AUTO_SCRAM.MATRIX_FILL
|
||||
self.status_text = { "AUTOMATIC SCRAM", "induction matrix fill high" }
|
||||
|
||||
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
|
||||
|
||||
log.info("FAC: automatic SCRAM due to induction matrix high charge")
|
||||
elseif astatus.gen_fault then
|
||||
-- lowest priority alarm
|
||||
next_mode = PROCESS.GEN_RATE_FAULT_IDLE
|
||||
self.ascram_reason = AUTO_SCRAM.GEN_FAULT
|
||||
self.status_text = { "GENERATION MODE IDLE", "paused: system not ready" }
|
||||
|
||||
log.info("FAC: automatic SCRAM due to unit problem while in GEN_RATE mode, will resume once all units are ready")
|
||||
end
|
||||
end
|
||||
|
||||
self.ascram = scram
|
||||
|
||||
if not self.ascram then
|
||||
self.ascram_reason = AUTO_SCRAM.NONE
|
||||
|
||||
-- reset PLC RPS trips if we should
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
u.auto_cond_rps_reset()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- update last mode and set next mode
|
||||
function update.post_auto()
|
||||
self.last_mode = self.mode
|
||||
self.mode = next_mode
|
||||
end
|
||||
|
||||
-- update alarm audio control
|
||||
function update.alarm_audio()
|
||||
local allow_test = self.allow_testing and self.test_tone_set
|
||||
|
||||
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
|
||||
-- reset tone states before re-evaluting
|
||||
for i = 1, #self.tone_states do self.tone_states[i] = false end
|
||||
|
||||
if allow_test then
|
||||
alarms = self.test_alarm_states
|
||||
else
|
||||
-- check all alarms for all units
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
for id, alarm in pairs(u.get_alarms()) do
|
||||
alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED)
|
||||
end
|
||||
end
|
||||
|
||||
if not self.test_tone_reset then
|
||||
-- clear testing alarms if we aren't using them
|
||||
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
|
||||
end
|
||||
end
|
||||
|
||||
-- Evaluate Alarms --
|
||||
|
||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||
if alarms[ALARM.ContainmentBreach] then
|
||||
self.tone_states[TONE.T_1800Hz_Int_4Hz] = true
|
||||
else
|
||||
-- critical damage is highest priority CRITICAL level alarm
|
||||
if alarms[ALARM.CriticalDamage] then
|
||||
self.tone_states[TONE.T_660Hz_Int_125ms] = true
|
||||
else
|
||||
-- EMERGENCY level alarms + URGENT over temp
|
||||
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
||||
self.tone_states[TONE.T_544Hz_440Hz_Alt] = true
|
||||
-- URGENT level turbine trip
|
||||
elseif alarms[ALARM.TurbineTrip] then
|
||||
self.tone_states[TONE.T_745Hz_Int_1Hz] = true
|
||||
-- URGENT level reactor lost
|
||||
elseif alarms[ALARM.ReactorLost] then
|
||||
self.tone_states[TONE.T_340Hz_Int_2Hz] = true
|
||||
-- TIMELY level alarms
|
||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||
self.tone_states[TONE.T_800Hz_Int] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- check RPS transient URGENT level alarm
|
||||
if alarms[ALARM.RPSTransient] then
|
||||
self.tone_states[TONE.T_1000Hz_Int] = true
|
||||
-- disable really painful audio combination
|
||||
self.tone_states[TONE.T_340Hz_Int_2Hz] = false
|
||||
end
|
||||
end
|
||||
|
||||
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
||||
if alarms[ALARM.ContainmentRadiation] then
|
||||
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
|
||||
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
|
||||
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
|
||||
if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end
|
||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
||||
self.tone_states[TONE.T_745Hz_Int_1Hz] = false
|
||||
self.tone_states[TONE.T_800Hz_Int] = false
|
||||
self.tone_states[TONE.T_1000Hz_Int] = false
|
||||
end
|
||||
|
||||
-- add to tone states if testing is active
|
||||
if allow_test then
|
||||
for i = 1, #self.tone_states do
|
||||
self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i]
|
||||
end
|
||||
|
||||
self.test_tone_reset = false
|
||||
else
|
||||
if not self.test_tone_reset then
|
||||
-- clear testing tones if we aren't using them
|
||||
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
|
||||
end
|
||||
|
||||
-- flag that tones were reset
|
||||
self.test_tone_set = false
|
||||
self.test_tone_reset = true
|
||||
end
|
||||
end
|
||||
|
||||
-- update facility redstone
|
||||
---@param ack_all function acknowledge all alarms
|
||||
function update.redstone(ack_all)
|
||||
if #self.redstone > 0 then
|
||||
-- handle facility SCRAM
|
||||
if self.io_ctl.digital_read(IO.F_SCRAM) then
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
u.cond_scram()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle facility ack
|
||||
if self.io_ctl.digital_read(IO.F_ACK) then ack_all() end
|
||||
|
||||
-- update facility alarm outputs
|
||||
local has_prio_alarm, has_any_alarm = false, false
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
|
||||
if u.has_alarm_min_prio(PRIO.EMERGENCY) then
|
||||
has_prio_alarm, has_any_alarm = true, true
|
||||
break
|
||||
elseif u.has_alarm_min_prio(PRIO.TIMELY) then
|
||||
has_any_alarm = true
|
||||
end
|
||||
end
|
||||
|
||||
self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm)
|
||||
self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
|
||||
|
||||
-- update induction matrix related outputs
|
||||
if self.induction[1] ~= nil then
|
||||
local db = self.induction[1].get_db() ---@type imatrix_session_db
|
||||
|
||||
self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW)
|
||||
self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH)
|
||||
self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- update unit tasks
|
||||
function update.unit_mgmt()
|
||||
local insufficent_po_rate = false
|
||||
local need_emcool = false
|
||||
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
|
||||
-- update auto waste processing
|
||||
if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then
|
||||
if (u.get_sna_rate() * 10.0) < u.get_burn_rate() then
|
||||
insufficent_po_rate = true
|
||||
end
|
||||
end
|
||||
|
||||
-- check if unit activated emergency coolant & uses facility tanks
|
||||
if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then
|
||||
need_emcool = true
|
||||
end
|
||||
end
|
||||
|
||||
-- update waste product
|
||||
|
||||
self.current_waste_product = self.waste_product
|
||||
|
||||
if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then
|
||||
local db = self.induction[1].get_db() ---@type imatrix_session_db
|
||||
|
||||
if db.tanks.energy_fill >= 0.15 then
|
||||
self.disabled_sps = false
|
||||
elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then
|
||||
self.disabled_sps = true
|
||||
self.current_waste_product = WASTE.POLONIUM
|
||||
end
|
||||
else
|
||||
self.disabled_sps = false
|
||||
end
|
||||
|
||||
if self.pu_fallback and insufficent_po_rate then
|
||||
self.current_waste_product = WASTE.PLUTONIUM
|
||||
end
|
||||
|
||||
-- make sure dynamic tanks are allowing outflow if required
|
||||
-- set all, rather than trying to determine which is for which (simpler & safer)
|
||||
-- there should be no need for any to be in fill only mode
|
||||
if need_emcool then
|
||||
for i = 1, #self.tanks do
|
||||
local session = self.tanks[i] ---@type unit_session
|
||||
local tank = session.get_db() ---@type dynamicv_session_db
|
||||
|
||||
if tank.state.container_mode == CONTAINER_MODE.FILL then
|
||||
session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- link the self instance and return the update interface
|
||||
---@param fac_self _facility_self
|
||||
return function (fac_self)
|
||||
self = fac_self
|
||||
return update
|
||||
end
|
||||
47
supervisor/panel/components/chk_entry.lua
Normal file
47
supervisor/panel/components/chk_entry.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
--
|
||||
-- RTU ID Check Failure Entry
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local style = require("supervisor.panel.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create an ID check list entry
|
||||
---@param parent graphics_element parent
|
||||
---@param msg string message
|
||||
---@param fail_code integer failure code
|
||||
local function init(parent, msg, fail_code)
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright}
|
||||
|
||||
local fg_bg = cpair(colors.black,colors.yellow)
|
||||
local tag = "MISSING"
|
||||
|
||||
if fail_code == types.RTU_ID_FAIL.OUT_OF_RANGE then
|
||||
fg_bg = cpair(colors.black,colors.orange)
|
||||
tag = "BAD INDEX"
|
||||
elseif fail_code == types.RTU_ID_FAIL.DUPLICATE then
|
||||
fg_bg = cpair(colors.black,colors.red)
|
||||
tag = "DUPLICATE"
|
||||
end
|
||||
|
||||
TextBox{parent=entry,y=1,text="",width=11,fg_bg=fg_bg}
|
||||
TextBox{parent=entry,text=tag,alignment=ALIGN.CENTER,width=11,fg_bg=fg_bg}
|
||||
TextBox{parent=entry,text="",width=11,fg_bg=fg_bg}
|
||||
|
||||
TextBox{parent=entry,x=13,y=2,text=msg}
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -10,6 +10,7 @@ local supervisor = require("supervisor.supervisor")
|
||||
local pgi = require("supervisor.panel.pgi")
|
||||
local style = require("supervisor.panel.style")
|
||||
|
||||
local chk_entry = require("supervisor.panel.components.chk_entry")
|
||||
local pdg_entry = require("supervisor.panel.components.pdg_entry")
|
||||
local rtu_entry = require("supervisor.panel.components.rtu_entry")
|
||||
|
||||
@@ -73,8 +74,8 @@ local function init(panel)
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,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)
|
||||
@@ -83,7 +84,7 @@ local function init(panel)
|
||||
-- page handling
|
||||
--
|
||||
|
||||
-- plc page
|
||||
-- plc sessions page
|
||||
|
||||
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
|
||||
@@ -115,13 +116,13 @@ local function init(panel)
|
||||
plc_list.line_break()
|
||||
end
|
||||
|
||||
-- rtu page
|
||||
-- rtu sessions page
|
||||
|
||||
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- coordinator page
|
||||
-- coordinator session page
|
||||
|
||||
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright}
|
||||
@@ -143,15 +144,37 @@ local function init(panel)
|
||||
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
|
||||
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
|
||||
|
||||
-- pocket diagnostics page
|
||||
-- pocket sessions page
|
||||
|
||||
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,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=pdg_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- RTU device ID check/diagnostics page
|
||||
|
||||
local chk_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,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=chk_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- info page
|
||||
|
||||
local info_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local info = Div{parent=info_page,height=6,x=2,y=2}
|
||||
|
||||
TextBox{parent=info,text="SVR \x1a Supervisor Status"}
|
||||
TextBox{parent=info,text="PLC \x1a Reactor PLC Connections"}
|
||||
TextBox{parent=info,text="RTU \x1a RTU Gateway Connections"}
|
||||
TextBox{parent=info,text="CRD \x1a Coordinator Connection"}
|
||||
TextBox{parent=info,text="PKT \x1a Pocket Connections"}
|
||||
TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"}
|
||||
|
||||
local notes = Div{parent=info_page,width=49,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
|
||||
|
||||
TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."}
|
||||
|
||||
-- assemble page panes
|
||||
|
||||
local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page }
|
||||
local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page, chk_page, info_page }
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
|
||||
@@ -161,12 +184,14 @@ local function init(panel)
|
||||
{ name = "RTU", color = style.fp.text },
|
||||
{ name = "CRD", color = style.fp.text },
|
||||
{ name = "PKT", color = style.fp.text },
|
||||
{ name = "DEV", color = style.fp.text },
|
||||
{ name = "INF", color = style.fp.text }
|
||||
}
|
||||
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright}
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=7,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright}
|
||||
|
||||
-- link RTU/PDG list management to PGI
|
||||
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
|
||||
-- link RTU/PDG/CHK list management to PGI
|
||||
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -10,10 +10,12 @@ local pgi = {}
|
||||
local data = {
|
||||
rtu_list = nil, ---@type nil|graphics_element
|
||||
pdg_list = nil, ---@type nil|graphics_element
|
||||
chk_list = nil, ---@type nil|graphics_element
|
||||
rtu_entry = nil, ---@type function
|
||||
pdg_entry = nil, ---@type function
|
||||
-- session entries
|
||||
s_entries = { rtu = {}, pdg = {} }
|
||||
chk_entry = nil, ---@type function
|
||||
-- list entries
|
||||
entries = { rtu = {}, pdg = {}, chk = {}, missing = {} }
|
||||
}
|
||||
|
||||
-- link list boxes
|
||||
@@ -21,19 +23,25 @@ local data = {
|
||||
---@param rtu_entry function RTU entry constructor
|
||||
---@param pdg_list graphics_element pocket diagnostics list element
|
||||
---@param pdg_entry function pocket diagnostics entry constructor
|
||||
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
|
||||
---@param chk_list graphics_element CHK list element
|
||||
---@param chk_entry function CHK entry constructor
|
||||
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
|
||||
data.rtu_list = rtu_list
|
||||
data.pdg_list = pdg_list
|
||||
data.chk_list = chk_list
|
||||
data.rtu_entry = rtu_entry
|
||||
data.pdg_entry = pdg_entry
|
||||
data.chk_entry = chk_entry
|
||||
end
|
||||
|
||||
-- unlink all fields, disabling the PGI
|
||||
function pgi.unlink()
|
||||
data.rtu_list = nil
|
||||
data.pdg_list = nil
|
||||
data.chk_list = nil
|
||||
data.rtu_entry = nil
|
||||
data.pdg_entry = nil
|
||||
data.chk_entry = nil
|
||||
end
|
||||
|
||||
-- add an RTU entry to the RTU list
|
||||
@@ -43,7 +51,8 @@ function pgi.create_rtu_entry(session_id)
|
||||
local success, result = pcall(data.rtu_entry, data.rtu_list, session_id)
|
||||
|
||||
if success then
|
||||
data.s_entries.rtu[session_id] = result
|
||||
data.entries.rtu[session_id] = result
|
||||
log.debug(util.c("PGI: created RTU entry (", session_id, ")"))
|
||||
else
|
||||
log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
|
||||
end
|
||||
@@ -53,15 +62,17 @@ end
|
||||
-- delete an RTU entry from the RTU list
|
||||
---@param session_id integer RTU session
|
||||
function pgi.delete_rtu_entry(session_id)
|
||||
if data.s_entries.rtu[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.rtu[session_id].delete)
|
||||
data.s_entries.rtu[session_id] = nil
|
||||
if data.entries.rtu[session_id] ~= nil then
|
||||
local success, result = pcall(data.entries.rtu[session_id].delete)
|
||||
data.entries.rtu[session_id] = nil
|
||||
|
||||
if not success then
|
||||
if success then
|
||||
log.debug(util.c("PGI: deleted RTU entry (", session_id, ")"))
|
||||
else
|
||||
log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
|
||||
end
|
||||
else
|
||||
log.debug(util.c("PGI: tried to delete unknown RTU entry ", session_id))
|
||||
log.warning(util.c("PGI: tried to delete unknown RTU entry ", session_id))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -72,7 +83,8 @@ function pgi.create_pdg_entry(session_id)
|
||||
local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
|
||||
|
||||
if success then
|
||||
data.s_entries.pdg[session_id] = result
|
||||
data.entries.pdg[session_id] = result
|
||||
log.debug(util.c("PGI: created PDG entry (", session_id, ")"))
|
||||
else
|
||||
log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
|
||||
end
|
||||
@@ -82,15 +94,92 @@ end
|
||||
-- delete a PDG entry from the PDG list
|
||||
---@param session_id integer pocket diagnostics session
|
||||
function pgi.delete_pdg_entry(session_id)
|
||||
if data.s_entries.pdg[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.pdg[session_id].delete)
|
||||
data.s_entries.pdg[session_id] = nil
|
||||
if data.entries.pdg[session_id] ~= nil then
|
||||
local success, result = pcall(data.entries.pdg[session_id].delete)
|
||||
data.entries.pdg[session_id] = nil
|
||||
|
||||
if not success then
|
||||
if success then
|
||||
log.debug(util.c("PGI: deleted PDG entry (", session_id, ")"))
|
||||
else
|
||||
log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
|
||||
end
|
||||
else
|
||||
log.debug(util.c("PGI: tried to delete unknown PDG entry ", session_id))
|
||||
log.warning(util.c("PGI: tried to delete unknown PDG entry ", session_id))
|
||||
end
|
||||
end
|
||||
|
||||
-- add a device ID check failure entry to the CHK list
|
||||
---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case
|
||||
---@param unit unit_session RTU session
|
||||
---@param fail_code integer failure code
|
||||
---@param msg string description to show the user
|
||||
function pgi.create_chk_entry(unit, fail_code, msg)
|
||||
local gw_session = unit.get_session_id()
|
||||
|
||||
if data.chk_list ~= nil and data.chk_entry ~= nil then
|
||||
if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end
|
||||
|
||||
local success, result = pcall(data.chk_entry, data.chk_list, msg, fail_code)
|
||||
|
||||
if success then
|
||||
data.entries.chk[gw_session][unit.get_unit_id()] = result
|
||||
log.debug(util.c("PGI: created CHK entry (", gw_session, ":", unit.get_unit_id(), ")"))
|
||||
else
|
||||
log.error(util.c("PGI: failed to create CHK entry (", result, ")"), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- delete a device ID check failure entry from the CHK list
|
||||
---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case
|
||||
---@param unit unit_session RTU session
|
||||
function pgi.delete_chk_entry(unit)
|
||||
local gw_session = unit.get_session_id()
|
||||
local ent_chk = data.entries.chk
|
||||
|
||||
if ent_chk[gw_session] ~= nil and ent_chk[gw_session][unit.get_unit_id()] ~= nil then
|
||||
local success, result = pcall(ent_chk[gw_session][unit.get_unit_id()].delete)
|
||||
ent_chk[gw_session][unit.get_unit_id()] = nil
|
||||
|
||||
if success then
|
||||
log.debug(util.c("PGI: deleted CHK entry ", gw_session, ":", unit.get_unit_id()))
|
||||
else
|
||||
log.error(util.c("PGI: failed to delete CHK entry (", result, ")"), true)
|
||||
end
|
||||
else
|
||||
log.warning(util.c("PGI: tried to delete unknown CHK entry with session of ", gw_session, " and unit ID of ", unit.get_unit_id()))
|
||||
end
|
||||
end
|
||||
|
||||
-- add a device ID missing entry to the CHK list
|
||||
---@param message string missing device message
|
||||
function pgi.create_missing_entry(message)
|
||||
if data.chk_list ~= nil and data.chk_entry ~= nil then
|
||||
local success, result = pcall(data.chk_entry, data.chk_list, message, 4)
|
||||
|
||||
if success then
|
||||
data.entries.missing[message] = result
|
||||
log.debug(util.c("PGI: created missing CHK entry (", message, ")"))
|
||||
else
|
||||
log.error(util.c("PGI: failed to create missing CHK entry (", result, ")"), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- delete a device ID missing entry from the CHK list
|
||||
---@param message string missing device message
|
||||
function pgi.delete_missing_entry(message)
|
||||
if data.entries.missing[message] ~= nil then
|
||||
local success, result = pcall(data.entries.missing[message].delete)
|
||||
data.entries.missing[message] = nil
|
||||
|
||||
if success then
|
||||
log.debug(util.c("PGI: deleted missing CHK entry \"", message, "\""))
|
||||
else
|
||||
log.error(util.c("PGI: failed to delete missing CHK entry (", result, ")"), true)
|
||||
end
|
||||
else
|
||||
log.warning(util.c("PGI: tried to delete unknown missing CHK entry \"", message, "\""))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 databus = require("supervisor.databus")
|
||||
@@ -15,6 +16,9 @@ local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local AUTO_GROUP = types.AUTO_GROUP
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
|
||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||
|
||||
-- retry time constants in ms
|
||||
@@ -53,7 +57,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||
|
||||
local log_header = "crdn_session(" .. id .. "): "
|
||||
local log_tag = "crdn_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
units = facility.get_units(),
|
||||
@@ -184,7 +188,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num() + 1
|
||||
@@ -205,7 +209,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "COORD KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_tag .. "COORD KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
|
||||
@@ -213,13 +217,17 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
|
||||
databus.tx_crd_rtt(self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == MGMT_TYPE.ESTABLISH then
|
||||
-- something is wrong, kill the session
|
||||
_close()
|
||||
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
@@ -237,11 +245,16 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
facility.scram_all()
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, true })
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
facility.auto_stop()
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, true })
|
||||
local was_active = facility.auto_is_active()
|
||||
|
||||
if was_active then
|
||||
facility.auto_stop()
|
||||
end
|
||||
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, was_active })
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if pkt.length == 6 then
|
||||
---@type coord_auto_config
|
||||
---@type sys_auto_config
|
||||
local config = {
|
||||
mode = pkt.data[2],
|
||||
burn_target = pkt.data[3],
|
||||
@@ -252,7 +265,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
facility.ack_all()
|
||||
@@ -261,25 +274,25 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
if pkt.length == 2 then
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set waste mode packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
if pkt.length == 2 then
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set pu fallback packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
if pkt.length == 2 then
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set sps low power packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command unknown")
|
||||
log.debug(log_tag .. "CRDN facility command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN facility command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.UNIT_BUILDS then
|
||||
-- acknowledgement to coordinator receiving builds
|
||||
@@ -295,25 +308,34 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
|
||||
-- continue if valid unit id
|
||||
if util.is_int(uid) and uid > 0 and uid <= #self.units then
|
||||
local unit = self.units[uid] ---@type reactor_unit
|
||||
local unit = self.units[uid] ---@type reactor_unit
|
||||
local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL
|
||||
|
||||
if cmd == UNIT_COMMAND.START then
|
||||
out_queue.push_data(SV_Q_DATA.START, data)
|
||||
elseif cmd == UNIT_COMMAND.SCRAM then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
out_queue.push_data(SV_Q_DATA.SCRAM, data)
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
if manual then
|
||||
out_queue.push_data(SV_Q_DATA.START, data)
|
||||
else
|
||||
-- denied
|
||||
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, false })
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
if pkt.length == 3 then
|
||||
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
|
||||
if manual then
|
||||
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command burn rate missing option")
|
||||
log.debug(log_tag .. "CRDN unit command burn rate missing option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
|
||||
(pkt.data[3] >= WASTE_MODE.AUTO) and (pkt.data[3] <= WASTE_MODE.MANUAL_ANTI_MATTER) then
|
||||
unit.set_waste_mode(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command set waste missing/invalid option")
|
||||
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_all()
|
||||
@@ -322,32 +344,32 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
if pkt.length == 3 then
|
||||
unit.ack_alarm(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
|
||||
log.debug(log_tag .. "CRDN unit command ack alarm missing alarm id")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.RESET_ALARM then
|
||||
if pkt.length == 3 then
|
||||
unit.reset_alarm(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
|
||||
log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
|
||||
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
|
||||
facility.set_group(unit.get_id(), pkt.data[3])
|
||||
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command set group missing group id")
|
||||
log.debug(log_tag .. "CRDN unit command set group missing group id")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command unknown")
|
||||
log.debug(log_tag .. "CRDN unit command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command invalid")
|
||||
log.debug(log_tag .. "CRDN unit command invalid")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN unit command packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -370,7 +392,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
println("connection to coordinator " .. id .. " closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -437,14 +459,14 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
_send(CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) })
|
||||
end
|
||||
else
|
||||
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||
log.error(log_tag .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -452,7 +474,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("connection to coordinator closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
log.info(log_tag .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||
|
||||
local log_header = "plc_session(" .. id .. "): "
|
||||
local log_tag = "plc_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
commanded_state = false,
|
||||
@@ -184,7 +184,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
|
||||
self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
|
||||
|
||||
log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
|
||||
log.info(util.sprintf(log_tag .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
|
||||
self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na))
|
||||
end
|
||||
|
||||
@@ -289,12 +289,12 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
_copy_status(pkt.data[7])
|
||||
self.received_status_cache = true
|
||||
else
|
||||
log.error(log_header .. "RPLC status packet reactor data length mismatch")
|
||||
log.error(log_tag .. "RPLC status packet reactor data length mismatch")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet invalid")
|
||||
log.debug(log_tag .. "RPLC status packet invalid")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -341,7 +341,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
if pkt.length == 1 then
|
||||
return pkt.data[1]
|
||||
else
|
||||
log.debug(log_header .. "RPLC ACK length mismatch")
|
||||
log.debug(log_tag .. "RPLC ACK length mismatch")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
@@ -351,7 +351,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num() + 1
|
||||
@@ -362,7 +362,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
---@cast pkt rplc_frame
|
||||
-- check reactor ID
|
||||
if pkt.id ~= reactor_id then
|
||||
log.warning(log_header .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
|
||||
log.warning(log_tag .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -375,7 +375,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
if pkt.length >= 5 then
|
||||
_handle_status(pkt)
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC status packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
|
||||
-- received reactor structure, record it
|
||||
@@ -385,7 +385,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.received_struct = true
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
|
||||
else
|
||||
log.debug(log_header .. "RPLC struct packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC struct packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
|
||||
-- burn rate acknowledgement
|
||||
@@ -393,22 +393,15 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
if ack then
|
||||
self.acks.burn_rate = true
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "burn rate update failed!")
|
||||
log.debug(log_tag .. "burn rate update failed!")
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = reactor_id,
|
||||
cmd = UNIT_COMMAND.SET_BURN,
|
||||
ack = ack
|
||||
})
|
||||
elseif pkt.type == RPLC_TYPE.RPS_ENABLE then
|
||||
-- enable acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if ack then
|
||||
self.sDB.control_state = true
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "enable failed!")
|
||||
log.debug(log_tag .. "enable failed!")
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
@@ -424,7 +417,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.acks.disable = true
|
||||
self.sDB.control_state = false
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "disable failed!")
|
||||
log.debug(log_tag .. "disable failed!")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
|
||||
-- manual SCRAM acknowledgement
|
||||
@@ -433,7 +426,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.acks.scram = true
|
||||
self.sDB.control_state = false
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "manual SCRAM failed!")
|
||||
log.debug(log_tag .. "manual SCRAM failed!")
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
@@ -449,7 +442,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.acks.ascram = true
|
||||
self.sDB.control_state = false
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. " automatic SCRAM failed!")
|
||||
log.debug(log_tag .. " automatic SCRAM failed!")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_STATUS then
|
||||
-- RPS status packet received, copy data
|
||||
@@ -459,10 +452,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
-- copied in RPS status data OK
|
||||
else
|
||||
-- error copying RPS status data
|
||||
log.error(log_header .. "failed to parse RPS status packet data")
|
||||
log.error(log_tag .. "failed to parse RPS status packet data")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "RPLC RPS status packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC RPS status packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_ALARM then
|
||||
-- RPS alarm
|
||||
@@ -472,10 +465,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
-- copied in RPS status data OK
|
||||
else
|
||||
-- error copying RPS status data
|
||||
log.error(log_header .. "failed to parse RPS alarm status data")
|
||||
log.error(log_tag .. "failed to parse RPS alarm status data")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "RPLC RPS alarm packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC RPS alarm packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_RESET then
|
||||
-- RPS reset acknowledgement
|
||||
@@ -485,7 +478,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.sDB.rps_tripped = false
|
||||
self.sDB.rps_trip_cause = "ok"
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "RPS reset failed")
|
||||
log.debug(log_tag .. "RPS reset failed")
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
@@ -498,7 +491,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
-- RPS auto control reset acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if not ack then
|
||||
log.debug(log_header .. "RPS auto reset failed")
|
||||
log.debug(log_tag .. "RPS auto reset failed")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then
|
||||
if pkt.length == 1 then
|
||||
@@ -506,18 +499,18 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
|
||||
if ack == PLC_AUTO_ACK.FAIL then
|
||||
self.acks.burn_rate = false
|
||||
log.debug(log_header .. "RPLC automatic burn rate set fail")
|
||||
log.debug(log_tag .. "RPLC automatic burn rate set fail")
|
||||
elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK or ack == PLC_AUTO_ACK.RAMP_SET_OK or ack == PLC_AUTO_ACK.ZERO_DIS_OK then
|
||||
self.acks.burn_rate = true
|
||||
else
|
||||
self.acks.burn_rate = false
|
||||
log.debug(log_header .. "RPLC automatic burn rate ack unknown")
|
||||
log.debug(log_tag .. "RPLC automatic burn rate ack unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC automatic burn rate ack packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported RPLC packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@cast pkt mgmt_frame
|
||||
@@ -530,7 +523,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_tag .. "PLC KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
|
||||
@@ -538,13 +531,17 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
|
||||
databus.tx_plc_rtt(reactor_id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == MGMT_TYPE.ESTABLISH then
|
||||
-- something is wrong, kill the session
|
||||
_close()
|
||||
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -639,7 +636,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
println("connection to reactor " .. reactor_id .. " PLC closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -696,7 +693,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
|
||||
end
|
||||
else
|
||||
log.error(log_header .. "unsupported command received in in_queue (this is a bug)", true)
|
||||
log.error(log_tag .. "unsupported command received in in_queue (this is a bug)", true)
|
||||
end
|
||||
elseif message.qtype == mqueue.TYPE.DATA then
|
||||
-- instruction with body
|
||||
@@ -745,14 +742,14 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
end
|
||||
end
|
||||
else
|
||||
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||
log.error(log_tag .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -760,7 +757,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("connection to reactor " .. reactor_id .. " PLC closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
log.info(log_tag .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
@@ -802,7 +799,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
|
||||
if not self.received_status_cache then
|
||||
if rtimes.status_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPE.MEK_STATUS, {})
|
||||
_send(RPLC_TYPE.STATUS, {})
|
||||
rtimes.status_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,7 +40,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||
|
||||
local log_header = "pdg_session(" .. id .. "): "
|
||||
local log_tag = "pdg_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
-- connection properties
|
||||
@@ -95,7 +95,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num() + 1
|
||||
@@ -116,7 +116,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_tag .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms")
|
||||
@@ -124,11 +124,15 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
|
||||
databus.tx_pdg_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == MGMT_TYPE.ESTABLISH then
|
||||
-- something is wrong, kill the session
|
||||
_close()
|
||||
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
elseif pkt.type == MGMT_TYPE.DIAG_TONE_GET then
|
||||
-- get the state of alarm tones
|
||||
_send_mgmt(MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones())
|
||||
@@ -145,13 +149,13 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2])
|
||||
_send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag tone set packet data type mismatch")
|
||||
log.debug(log_tag .. "SCADA diag tone set packet data type mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag tone set packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA diag tone set packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security")
|
||||
log.debug(log_tag .. "DIAG_TONE_SET is blocked without HMAC for security")
|
||||
end
|
||||
|
||||
if not valid then _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { false }) end
|
||||
@@ -168,18 +172,18 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2])
|
||||
_send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag alarm set packet data type mismatch")
|
||||
log.debug(log_tag .. "SCADA diag alarm set packet data type mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag alarm set packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA diag alarm set packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security")
|
||||
log.debug(log_tag .. "DIAG_ALARM_SET is blocked without HMAC for security")
|
||||
end
|
||||
|
||||
if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -205,7 +209,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
println("connection to pocket diag session " .. id .. " closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -236,7 +240,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -244,7 +248,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("connection to pocket diag session " .. id .. " closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
log.info(log_tag .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ local PERIODICS = {
|
||||
ALARM_TONES = 500
|
||||
}
|
||||
|
||||
-- create a new RTU session
|
||||
-- create a new RTU gateway session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param s_addr integer device source address
|
||||
@@ -38,14 +38,14 @@ local PERIODICS = {
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
---@param advertisement table RTU device advertisement
|
||||
---@param advertisement table RTU gateway device advertisement
|
||||
---@param facility facility facility data table
|
||||
---@param fp_ok boolean if the front panel UI is running
|
||||
function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||
|
||||
local log_header = "rtu_session(" .. id .. "): "
|
||||
local log_tag = "rtu_gw_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
modbus_q = mqueue.new(),
|
||||
@@ -124,7 +124,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
|
||||
if u_type == false then
|
||||
-- validation fail
|
||||
log.debug(log_header .. "_handle_advertisement(): advertisement unit validation failure")
|
||||
log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
|
||||
else
|
||||
if unit_advert.reactor > 0 then
|
||||
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
|
||||
@@ -156,9 +156,9 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
if type(unit) ~= "nil" then target_unit.add_envd(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
-- skip virtual units
|
||||
log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
|
||||
log.debug(util.c(log_tag, "skipping virtual RTU #", i))
|
||||
else
|
||||
log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string))
|
||||
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string))
|
||||
end
|
||||
else
|
||||
-- facility RTUs
|
||||
@@ -184,9 +184,9 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
if type(unit) ~= "nil" then facility.add_envd(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
-- skip virtual units
|
||||
log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
|
||||
log.debug(util.c(log_tag, "skipping virtual RTU #", i))
|
||||
else
|
||||
log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string))
|
||||
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -195,20 +195,20 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
self.units[i] = unit
|
||||
unit_count = unit_count + 1
|
||||
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
|
||||
log.warning(util.c(log_header, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")"))
|
||||
log.warning(util.c(log_tag, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")"))
|
||||
end
|
||||
end
|
||||
|
||||
databus.tx_rtu_units(id, unit_count)
|
||||
end
|
||||
|
||||
-- mark this RTU session as closed, stop watchdog
|
||||
-- mark this RTU gateway session as closed, stop watchdog
|
||||
local function _close()
|
||||
self.conn_watchdog.cancel()
|
||||
self.connected = false
|
||||
databus.tx_rtu_disconnected(id)
|
||||
|
||||
-- mark all RTU unit sessions as closed so the reactor unit knows
|
||||
-- mark all RTU sessions as closed so the reactor unit knows
|
||||
for _, unit in pairs(self.units) do unit.close() end
|
||||
end
|
||||
|
||||
@@ -242,7 +242,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
|
||||
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
|
||||
return
|
||||
else
|
||||
self.r_seq_num = pkt.scada_frame.seq_num() + 1
|
||||
@@ -265,27 +265,31 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
-- local rtu_send = pkt.data[2]
|
||||
-- local rtu_gw_send = pkt.data[2]
|
||||
local srv_now = util.time()
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_tag .. "RTU GW KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
|
||||
-- log.debug(log_tag .. "RTU GW RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_tag .. "RTU GW TT = " .. (srv_now - rtu_gw_send) .. "ms")
|
||||
|
||||
databus.tx_rtu_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == MGMT_TYPE.ESTABLISH then
|
||||
-- something is wrong, kill the session
|
||||
_close()
|
||||
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
elseif pkt.type == MGMT_TYPE.RTU_ADVERT then
|
||||
-- RTU unit advertisement
|
||||
log.debug(log_header .. "received updated advertisement")
|
||||
-- RTU advertisement
|
||||
log.debug(log_tag .. "received updated advertisement")
|
||||
self.advert = pkt.data
|
||||
|
||||
-- handle advertisement; this will re-create all unit sub-sessions
|
||||
@@ -298,17 +302,17 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
unit.invalidate_cache()
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "SCADA RTU device re-mount packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA RTU GW device re-mount packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- get the session ID
|
||||
-- get the gateway session ID
|
||||
function public.get_id() return id end
|
||||
|
||||
-- check if a timer matches this session's watchdog
|
||||
@@ -322,8 +326,8 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
println(log_header .. "connection to RTU closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
println(log_tag .. "connection to RTU GW closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
@@ -354,7 +358,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning(log_header .. "exceeded 100ms queue process limit")
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -362,7 +366,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("RTU connection " .. id .. " closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
log.info(log_tag .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new boilerv rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function boilerv.new(session_id, unit_id, advert, out_queue)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
|
||||
@@ -44,10 +44,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new dynamicv rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function dynamicv.new(session_id, unit_id, advert, out_queue)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
|
||||
@@ -23,10 +23,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new environment detector rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer
|
||||
---@param unit_id integer
|
||||
---@param advert rtu_advertisement
|
||||
---@param out_queue mqueue
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function envd.new(session_id, unit_id, advert, out_queue)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
|
||||
@@ -32,10 +32,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new imatrix rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function imatrix.new(session_id, unit_id, advert, out_queue)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
|
||||
|
||||
@@ -45,10 +45,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new redstone rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer
|
||||
---@param unit_id integer
|
||||
---@param advert rtu_advertisement
|
||||
---@param out_queue mqueue
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
|
||||
|
||||
@@ -29,10 +29,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new sna rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function sna.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.SNA then
|
||||
|
||||
@@ -32,10 +32,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new sps rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function sps.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.SPS then
|
||||
|
||||
@@ -44,10 +44,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new turbinev rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function turbinev.new(session_id, unit_id, advert, out_queue)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
|
||||
@@ -24,7 +24,7 @@ unit_session.RTU_US_DATA = RTU_US_DATA
|
||||
|
||||
-- create a new unit session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer MODBUS unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement for this unit
|
||||
---@param out_queue mqueue send queue
|
||||
@@ -144,12 +144,15 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- get the unit ID
|
||||
-- get the RTU gateway session ID
|
||||
---@nodiscard
|
||||
function public.get_session_id() return session_id end
|
||||
-- get the unit ID
|
||||
---@nodiscard
|
||||
function public.get_unit_id() return unit_id end
|
||||
-- get the RTU type
|
||||
---@nodiscard
|
||||
function public.get_unit_type() return advert.type end
|
||||
-- get the device index
|
||||
---@nodiscard
|
||||
function public.get_device_idx() return self.device_index or 0 end
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
--
|
||||
-- Supervisor Sessions Handler
|
||||
--
|
||||
|
||||
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 databus = require("supervisor.databus")
|
||||
local facility = require("supervisor.facility")
|
||||
|
||||
local pgi = require("supervisor.panel.pgi")
|
||||
|
||||
local coordinator = require("supervisor.session.coordinator")
|
||||
local plc = require("supervisor.session.plc")
|
||||
@@ -11,13 +17,15 @@ local pocket = require("supervisor.session.pocket")
|
||||
local rtu = require("supervisor.session.rtu")
|
||||
local svqtypes = require("supervisor.session.svqtypes")
|
||||
|
||||
-- Supervisor Sessions Handler
|
||||
local RTU_ID_FAIL = types.RTU_ID_FAIL
|
||||
local RTU_TYPES = types.RTU_UNIT_TYPE
|
||||
|
||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
local PLC_S_DATA = plc.PLC_S_DATA
|
||||
local CRD_S_DATA = coordinator.CRD_S_DATA
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
local PLC_S_DATA = plc.PLC_S_DATA
|
||||
|
||||
local CRD_S_DATA = coordinator.CRD_S_DATA
|
||||
|
||||
local svsessions = {}
|
||||
|
||||
@@ -37,12 +45,13 @@ local self = {
|
||||
config = nil, ---@type svr_config
|
||||
facility = nil, ---@type facility|nil
|
||||
sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
|
||||
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }
|
||||
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 },
|
||||
dev_dbg = { duplicate = {}, out_of_range = {}, connected = {} }
|
||||
}
|
||||
|
||||
---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
--#region PRIVATE FUNCTIONS
|
||||
|
||||
-- handle a session output queue
|
||||
---@param session sv_session_structs
|
||||
@@ -190,18 +199,184 @@ local function _find_session(list, s_addr)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
-- periodically remove disconnected RTU gateway's RTU ID warnings and update the missing device list
|
||||
local function _update_dev_dbg()
|
||||
-- remove disconnected units from check failures lists
|
||||
|
||||
local f = function (unit) return unit.is_connected() end
|
||||
|
||||
util.filter_table(self.dev_dbg.duplicate, f, pgi.delete_chk_entry)
|
||||
util.filter_table(self.dev_dbg.out_of_range, f, pgi.delete_chk_entry)
|
||||
|
||||
-- update missing list
|
||||
|
||||
local conns = self.dev_dbg.connected
|
||||
local units = self.facility.get_units()
|
||||
local rtu_conns = self.facility.check_rtu_conns()
|
||||
|
||||
local function report(disconnected, msg)
|
||||
if disconnected then pgi.create_missing_entry(msg) else pgi.delete_missing_entry(msg) end
|
||||
end
|
||||
|
||||
-- look for disconnected facility RTUs
|
||||
|
||||
if rtu_conns.induction ~= conns.induction then
|
||||
report(conns.induction, util.c("the facility's induction matrix"))
|
||||
conns.induction = rtu_conns.induction
|
||||
end
|
||||
|
||||
if rtu_conns.sps ~= conns.sps then
|
||||
report(conns.sps, util.c("the facility's SPS"))
|
||||
conns.sps = rtu_conns.sps
|
||||
end
|
||||
|
||||
for i = 1, #conns.tanks do
|
||||
if (rtu_conns.tanks[i] or false) ~= conns.tanks[i] then
|
||||
report(conns.tanks[i], util.c("the facility's #", i, " dynamic tank"))
|
||||
conns.tanks[i] = rtu_conns.tanks[i] or false
|
||||
end
|
||||
end
|
||||
|
||||
-- look for disconnected unit RTUs
|
||||
|
||||
for u = 1, #units do
|
||||
local u_conns = conns.units[u]
|
||||
|
||||
rtu_conns = units[u].check_rtu_conns()
|
||||
|
||||
for i = 1, #u_conns.boilers do
|
||||
if (rtu_conns.boilers[i] or false) ~= u_conns.boilers[i] then
|
||||
report(u_conns.boilers[i], util.c("unit ", u, "'s #", i, " boiler"))
|
||||
u_conns.boilers[i] = rtu_conns.boilers[i] or false
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #u_conns.turbines do
|
||||
if (rtu_conns.turbines[i] or false) ~= u_conns.turbines[i] then
|
||||
report(u_conns.turbines[i], util.c("unit ", u, "'s #", i, " turbine"))
|
||||
u_conns.turbines[i] = rtu_conns.turbines[i] or false
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #u_conns.tanks do
|
||||
if (rtu_conns.tanks[i] or false) ~= u_conns.tanks[i] then
|
||||
report(u_conns.tanks[i], util.c("unit ", u, "'s dynamic tank"))
|
||||
u_conns.tanks[i] = rtu_conns.tanks[i] or false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region PUBLIC FUNCTIONS
|
||||
|
||||
-- on attempted link of an RTU to a facility or unit object, verify its ID and report a problem if it can't be accepted
|
||||
---@param unit unit_session RTU session
|
||||
---@param list table table of RTU sessions
|
||||
---@param max integer max of this type of RTU
|
||||
---@return RTU_ID_FAIL fail_code, string fail_str
|
||||
function svsessions.check_rtu_id(unit, list, max)
|
||||
local fail_code, fail_str = RTU_ID_FAIL.OK, "OK"
|
||||
|
||||
if (unit.get_device_idx() < 1 and max ~= 1) or unit.get_device_idx() > max then
|
||||
-- out-of-range
|
||||
fail_code, fail_str = RTU_ID_FAIL.OUT_OF_RANGE, "index out of range"
|
||||
table.insert(self.dev_dbg.out_of_range, unit)
|
||||
else
|
||||
for _, u in ipairs(list) do
|
||||
if u.get_device_idx() == unit.get_device_idx() then
|
||||
-- duplicate
|
||||
fail_code, fail_str = RTU_ID_FAIL.DUPLICATE, "duplicate index"
|
||||
table.insert(self.dev_dbg.duplicate, unit)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- make sure this won't exceed the maximum allowable devices
|
||||
if fail_code == RTU_ID_FAIL.OK and #list >= max then
|
||||
fail_code, fail_str = RTU_ID_FAIL.MAX_DEVICES, "too many of this type"
|
||||
end
|
||||
|
||||
-- add to the list for the user
|
||||
if fail_code ~= RTU_ID_FAIL.OK and fail_code ~= RTU_ID_FAIL.MAX_DEVICES then
|
||||
local r_id, idx, type = unit.get_reactor(), unit.get_device_idx(), unit.get_unit_type()
|
||||
local msg
|
||||
|
||||
if r_id == 0 then
|
||||
msg = "the facility's "
|
||||
|
||||
if type == RTU_TYPES.IMATRIX then
|
||||
msg = msg .. "induction matrix"
|
||||
elseif type == RTU_TYPES.SPS then
|
||||
msg = msg .. "SPS"
|
||||
elseif type == RTU_TYPES.DYNAMIC_VALVE then
|
||||
msg = util.c(msg, "#", idx, " dynamic tank")
|
||||
elseif type == RTU_TYPES.ENV_DETECTOR then
|
||||
msg = util.c(msg, "#", idx, " env. detector")
|
||||
else
|
||||
msg = msg .. " ? (error)"
|
||||
end
|
||||
else
|
||||
msg = util.c("unit ", r_id, "'s ")
|
||||
|
||||
if type == RTU_TYPES.BOILER_VALVE then
|
||||
msg = util.c(msg, "#", idx, " boiler")
|
||||
elseif type == RTU_TYPES.TURBINE_VALVE then
|
||||
msg = util.c(msg, "#", idx, " turbine")
|
||||
elseif type == RTU_TYPES.DYNAMIC_VALVE then
|
||||
msg = msg .. "dynamic tank"
|
||||
elseif type == RTU_TYPES.ENV_DETECTOR then
|
||||
msg = util.c(msg, "#", idx, " env. detector")
|
||||
else
|
||||
msg = msg .. " ? (error)"
|
||||
end
|
||||
end
|
||||
|
||||
pgi.create_chk_entry(unit, fail_code, msg)
|
||||
end
|
||||
|
||||
return fail_code, fail_str
|
||||
end
|
||||
|
||||
-- initialize svsessions
|
||||
---@param nic nic network interface device
|
||||
---@param fp_ok boolean front panel active
|
||||
---@param config svr_config supervisor configuration
|
||||
---@param cooling_conf sv_cooling_conf cooling configuration definition
|
||||
function svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||
---@param facility facility
|
||||
function svsessions.init(nic, fp_ok, config, facility)
|
||||
self.nic = nic
|
||||
self.fp_ok = fp_ok
|
||||
self.config = config
|
||||
self.facility = facility.new(config, cooling_conf)
|
||||
self.facility = facility
|
||||
|
||||
-- initialize connection tracking table by setting all expected devices to true
|
||||
-- if connections are missing, missing entries will then be created on the next update
|
||||
|
||||
self.dev_dbg.connected = { induction = true, sps = true, tanks = {}, units = {} }
|
||||
|
||||
local cool_conf = facility.get_cooling_conf()
|
||||
|
||||
for i = 1, #cool_conf.fac_tank_list do
|
||||
if cool_conf.fac_tank_list[i] == 2 then
|
||||
table.insert(self.dev_dbg.connected.tanks, true)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, config.UnitCount do
|
||||
local r_cool = cool_conf.r_cool[i]
|
||||
local conns = { boilers = {}, turbines = {}, tanks = {} }
|
||||
|
||||
for b = 1, r_cool.BoilerCount do conns.boilers[b] = true end
|
||||
for t = 1, r_cool.TurbineCount do conns.turbines[t] = true end
|
||||
|
||||
if r_cool.TankConnection and cool_conf.fac_tank_defs[i] == 1 then
|
||||
conns.tanks[1] = true
|
||||
end
|
||||
|
||||
self.dev_dbg.connected.units[i] = conns
|
||||
end
|
||||
end
|
||||
|
||||
-- find an RTU session by the computer ID
|
||||
@@ -466,6 +641,9 @@ function svsessions.iterate_all()
|
||||
|
||||
-- iterate units
|
||||
self.facility.update_units()
|
||||
|
||||
-- update tracking of bad RTU IDs and missing devices
|
||||
_update_dev_dbg()
|
||||
end
|
||||
|
||||
-- delete all closed sessions
|
||||
@@ -482,4 +660,6 @@ function svsessions.close_all()
|
||||
svsessions.free_all_closed()
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return svsessions
|
||||
|
||||
@@ -16,12 +16,13 @@ local core = require("graphics.core")
|
||||
|
||||
local configure = require("supervisor.configure")
|
||||
local databus = require("supervisor.databus")
|
||||
local facility = require("supervisor.facility")
|
||||
local renderer = require("supervisor.renderer")
|
||||
local supervisor = require("supervisor.supervisor")
|
||||
|
||||
local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local SUPERVISOR_VERSION = "v1.4.0"
|
||||
local SUPERVISOR_VERSION = "v1.5.3"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -129,9 +130,12 @@ local function main()
|
||||
println_ts = function (_) end
|
||||
end
|
||||
|
||||
-- create facility and unit objects
|
||||
local sv_facility = facility.new(config)
|
||||
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok)
|
||||
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok, sv_facility)
|
||||
|
||||
-- base loop clock (6.67Hz, 3 ticks)
|
||||
local MAIN_CLOCK = 0.15
|
||||
|
||||
@@ -102,14 +102,12 @@ end
|
||||
---@param _version string supervisor version
|
||||
---@param nic nic network interface device
|
||||
---@param fp_ok boolean if the front panel UI is running
|
||||
---@param facility facility facility instance
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function supervisor.comms(_version, nic, fp_ok)
|
||||
function supervisor.comms(_version, nic, fp_ok, facility)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||
|
||||
---@class sv_cooling_conf
|
||||
local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs }
|
||||
|
||||
local self = {
|
||||
last_est_acks = {}
|
||||
}
|
||||
@@ -122,8 +120,8 @@ function supervisor.comms(_version, nic, fp_ok)
|
||||
nic.closeAll()
|
||||
nic.open(config.SVR_Channel)
|
||||
|
||||
-- pass modem, status, and config data to svsessions
|
||||
svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||
-- pass system data and objects to svsessions
|
||||
svsessions.init(nic, fp_ok, config, facility)
|
||||
|
||||
-- send an establish request response
|
||||
---@param packet scada_packet
|
||||
@@ -373,7 +371,7 @@ function supervisor.comms(_version, nic, fp_ok)
|
||||
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
|
||||
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
|
||||
|
||||
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf })
|
||||
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, facility.get_cooling_conf() })
|
||||
else
|
||||
if last_ack ~= ESTABLISH_ACK.COLLISION then
|
||||
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local logic = require("supervisor.unitlogic")
|
||||
local logic = require("supervisor.unitlogic")
|
||||
|
||||
local plc = require("supervisor.session.plc")
|
||||
local rsctl = require("supervisor.session.rsctl")
|
||||
local plc = require("supervisor.session.plc")
|
||||
local rsctl = require("supervisor.session.rsctl")
|
||||
local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
@@ -14,6 +15,7 @@ local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local RTU_ID_FAIL = types.RTU_ID_FAIL
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
@@ -68,6 +70,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
-- time (ms) to idle for auto idling
|
||||
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
|
||||
|
||||
local log_tag = "UNIT " .. reactor_id .. ": "
|
||||
|
||||
---@class _unit_self
|
||||
local self = {
|
||||
r_id = reactor_id,
|
||||
@@ -264,7 +268,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
table.insert(self.db.annunciator.TurbineOverSpeed, false)
|
||||
table.insert(self.db.annunciator.GeneratorTrip, false)
|
||||
table.insert(self.db.annunciator.TurbineTrip, false)
|
||||
table.insert(self.turbine_stability_data, { time_state = 0, time_tanks = 0, rotation = 1 })
|
||||
table.insert(self.turbine_stability_data, { time_state = 0, time_tanks = 0, rotation = 1, input_rate = 0 })
|
||||
end
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
@@ -420,6 +424,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
self.plc_s = plc_session
|
||||
self.plc_i = plc_session.instance
|
||||
|
||||
log.debug(util.c(log_tag, "linked PLC [", plc_session.s_addr, ":", plc_session.r_chan, "]"))
|
||||
|
||||
-- reset deltas
|
||||
_reset_dt(DT_KEYS.ReactorTemp)
|
||||
_reset_dt(DT_KEYS.ReactorFuel)
|
||||
@@ -432,6 +438,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
---@param rs_unit unit_session
|
||||
function public.add_redstone(rs_unit)
|
||||
table.insert(self.redstone, rs_unit)
|
||||
log.debug(util.c(log_tag, "linked redstone [", rs_unit.get_unit_id(), "@", rs_unit.get_session_id(), "]"))
|
||||
|
||||
-- send or re-send waste settings
|
||||
_set_waste_valves(self.waste_product)
|
||||
@@ -441,42 +448,61 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
---@param turbine unit_session
|
||||
---@return boolean linked turbine accepted to associated device slot
|
||||
function public.add_turbine(turbine)
|
||||
if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then
|
||||
local fail_code, fail_str = svsessions.check_rtu_id(turbine, self.turbines, num_turbines)
|
||||
local ok = fail_code == RTU_ID_FAIL.OK
|
||||
|
||||
if ok then
|
||||
table.insert(self.turbines, turbine)
|
||||
log.debug(util.c(log_tag, "linked turbine #", turbine.get_device_idx(), " [", turbine.get_unit_id(), "@", turbine.get_session_id(), "]"))
|
||||
|
||||
-- reset deltas
|
||||
_reset_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx())
|
||||
_reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx())
|
||||
else
|
||||
log.warning(util.c(log_tag, "rejected turbine linking due to failure code ", fail_code, " (", fail_str, ")"))
|
||||
end
|
||||
|
||||
return true
|
||||
else return false end
|
||||
return ok
|
||||
end
|
||||
|
||||
-- link a boiler RTU session
|
||||
---@param boiler unit_session
|
||||
---@return boolean linked boiler accepted to associated device slot
|
||||
function public.add_boiler(boiler)
|
||||
if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then
|
||||
local fail_code, fail_str = svsessions.check_rtu_id(boiler, self.boilers, num_boilers)
|
||||
local ok = fail_code == RTU_ID_FAIL.OK
|
||||
|
||||
if ok then
|
||||
table.insert(self.boilers, boiler)
|
||||
log.debug(util.c(log_tag, "linked boiler #", boiler.get_device_idx(), " [", boiler.get_unit_id(), "@", boiler.get_session_id(), "]"))
|
||||
|
||||
-- reset deltas
|
||||
_reset_dt(DT_KEYS.BoilerWater .. boiler.get_device_idx())
|
||||
_reset_dt(DT_KEYS.BoilerSteam .. boiler.get_device_idx())
|
||||
_reset_dt(DT_KEYS.BoilerCCool .. boiler.get_device_idx())
|
||||
_reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx())
|
||||
else
|
||||
log.warning(util.c(log_tag, "rejected boiler linking due to failure code ", fail_code, " (", fail_str, ")"))
|
||||
end
|
||||
|
||||
return true
|
||||
else return false end
|
||||
return ok
|
||||
end
|
||||
|
||||
-- link a dynamic tank RTU session
|
||||
---@param dynamic_tank unit_session
|
||||
---@return boolean linked dynamic tank accepted (max 1)
|
||||
function public.add_tank(dynamic_tank)
|
||||
if #self.tanks == 0 then
|
||||
local fail_code, fail_str = svsessions.check_rtu_id(dynamic_tank, self.tanks, 1)
|
||||
local ok = fail_code == RTU_ID_FAIL.OK
|
||||
|
||||
if ok then
|
||||
table.insert(self.tanks, dynamic_tank)
|
||||
return true
|
||||
else return false end
|
||||
log.debug(util.c(log_tag, "linked dynamic tank [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]"))
|
||||
else
|
||||
log.warning(util.c(log_tag, "rejected dynamic tank linking due to failure code ", fail_code, " (", fail_str, ")"))
|
||||
end
|
||||
|
||||
return ok
|
||||
end
|
||||
|
||||
-- link a solar neutron activator RTU session
|
||||
@@ -485,12 +511,19 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
|
||||
-- link an environment detector RTU session
|
||||
---@param envd unit_session
|
||||
---@return boolean linked environment detector accepted (max 1)
|
||||
---@return boolean linked environment detector accepted
|
||||
function public.add_envd(envd)
|
||||
if #self.envd == 0 then
|
||||
local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99)
|
||||
local ok = fail_code == RTU_ID_FAIL.OK
|
||||
|
||||
if ok then
|
||||
table.insert(self.envd, envd)
|
||||
return true
|
||||
else return false end
|
||||
log.debug(util.c(log_tag, "linked environment detector #", envd.get_device_idx(), " [", envd.get_unit_id(), "@", envd.get_session_id(), "]"))
|
||||
else
|
||||
log.warning(util.c(log_tag, "rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")"))
|
||||
end
|
||||
|
||||
return ok
|
||||
end
|
||||
|
||||
-- purge devices associated with the given RTU session ID
|
||||
@@ -512,7 +545,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
self.db.control.br100 = 0
|
||||
end
|
||||
|
||||
-- unlink RTU unit sessions if they are closed
|
||||
-- unlink RTU sessions if they are closed
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
||||
|
||||
-- update degraded state for auto control
|
||||
@@ -547,7 +580,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
|
||||
-- stop idling when completed
|
||||
if self.auto_idling and (((util.time_ms() - self.auto_idle_start) > IDLE_TIME) or not self.auto_idle) then
|
||||
log.info(util.c("UNIT ", self.r_id, ": completed idling period"))
|
||||
log.info(util.c(log_tag, "completed idling period"))
|
||||
self.auto_idling = false
|
||||
self.plc_i.auto_set_burn(0, false)
|
||||
end
|
||||
@@ -584,7 +617,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
function public.auto_engage()
|
||||
self.auto_engaged = true
|
||||
if self.plc_i ~= nil then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": engaged auto control"))
|
||||
log.debug(util.c(log_tag, "engaged auto control"))
|
||||
self.plc_i.auto_lock(true)
|
||||
end
|
||||
end
|
||||
@@ -593,7 +626,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
function public.auto_disengage()
|
||||
self.auto_engaged = false
|
||||
if self.plc_i ~= nil then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control"))
|
||||
log.debug(util.c(log_tag, "disengaged auto control"))
|
||||
self.plc_i.auto_lock(false)
|
||||
self.db.control.br100 = 0
|
||||
end
|
||||
@@ -610,7 +643,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
end
|
||||
|
||||
if idle ~= self.auto_idle then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": idling mode changed to ", idle))
|
||||
log.debug(util.c(log_tag, "idling mode changed to ", idle))
|
||||
end
|
||||
|
||||
self.auto_idle = idle
|
||||
@@ -623,7 +656,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
function public.auto_get_effective_limit()
|
||||
local ctrl = self.db.control
|
||||
if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then
|
||||
-- log.debug(util.c("UNIT ", self.r_id, ": effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]"))
|
||||
-- log.debug(util.c(log_tag, "effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]"))
|
||||
ctrl.br100 = 0
|
||||
return 0
|
||||
else return ctrl.lim_br100 end
|
||||
@@ -634,7 +667,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
function public.auto_commit_br100(ramp)
|
||||
if self.auto_engaged then
|
||||
if self.plc_i ~= nil then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp))
|
||||
log.debug(util.c(log_tag, "commit br100 of ", self.db.control.br100, " with ramp set to ", ramp))
|
||||
|
||||
local rate = self.db.control.br100 / 100
|
||||
|
||||
@@ -643,16 +676,16 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
if self.auto_idle_start == 0 then
|
||||
self.auto_idling = true
|
||||
self.auto_idle_start = util.time_ms()
|
||||
log.info(util.c("UNIT ", self.r_id, ": started idling at ", IDLE_RATE, " mB/t"))
|
||||
log.info(util.c(log_tag, "started idling at ", IDLE_RATE, " mB/t"))
|
||||
|
||||
rate = IDLE_RATE
|
||||
elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then
|
||||
if self.auto_idling then
|
||||
self.auto_idling = false
|
||||
log.info(util.c("UNIT ", self.r_id, ": completed idling period"))
|
||||
log.info(util.c(log_tag, "completed idling period"))
|
||||
end
|
||||
else
|
||||
log.debug(util.c("UNIT ", self.r_id, ": continuing idle at ", IDLE_RATE, " mB/t"))
|
||||
log.debug(util.c(log_tag, "continuing idle at ", IDLE_RATE, " mB/t"))
|
||||
|
||||
rate = IDLE_RATE
|
||||
end
|
||||
@@ -891,6 +924,29 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
return rate or 0
|
||||
end
|
||||
|
||||
-- check which RTUs are connected
|
||||
---@nodiscard
|
||||
function public.check_rtu_conns()
|
||||
local conns = {}
|
||||
|
||||
conns.boilers = {}
|
||||
for i = 1, #self.boilers do
|
||||
conns.boilers[self.boilers[i].get_device_idx()] = true
|
||||
end
|
||||
|
||||
conns.turbines = {}
|
||||
for i = 1, #self.turbines do
|
||||
conns.turbines[self.turbines[i].get_device_idx()] = true
|
||||
end
|
||||
|
||||
conns.tanks = {}
|
||||
for i = 1, #self.tanks do
|
||||
conns.tanks[self.tanks[i].get_device_idx()] = true
|
||||
end
|
||||
|
||||
return conns
|
||||
end
|
||||
|
||||
-- get RTU statuses
|
||||
---@nodiscard
|
||||
function public.get_rtu_statuses()
|
||||
|
||||
@@ -84,7 +84,7 @@ function logic.update_annunciator(self)
|
||||
self.turbine_flow_stable = false
|
||||
|
||||
for t = 1, self.num_turbines do
|
||||
self.turbine_stability_data[t] = { time_state = 0, time_tanks = 0, rotation = 1 }
|
||||
self.turbine_stability_data[t] = { time_state = 0, time_tanks = 0, rotation = 1, input_rate = 0 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -317,7 +317,7 @@ function logic.update_annunciator(self)
|
||||
|
||||
local last = self.turbine_stability_data[i]
|
||||
|
||||
if (not self.turbine_flow_stable) and (turbine.state.steam_input_rate > 0) then
|
||||
if not self.turbine_flow_stable then
|
||||
local rotation = util.turbine_rotation(turbine)
|
||||
local rotation_stable = false
|
||||
|
||||
@@ -351,13 +351,18 @@ function logic.update_annunciator(self)
|
||||
end
|
||||
|
||||
turbines_stable = turbines_stable and (rotation_stable or flow_stable)
|
||||
else
|
||||
elseif math.abs(turbine.state.steam_input_rate - last.input_rate) > 1 then
|
||||
-- reset to unstable to re-check
|
||||
last.time_state = 0
|
||||
last.time_tanks = 0
|
||||
last.rotation = 1
|
||||
|
||||
turbines_stable = false
|
||||
|
||||
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reset stability (new rate ", turbine.state.steam_input_rate, " != ", last.input_rate," mB/t)"))
|
||||
end
|
||||
|
||||
last.input_rate = turbine.state.steam_input_rate
|
||||
end
|
||||
|
||||
self.turbine_flow_stable = self.turbine_flow_stable or turbines_stable
|
||||
|
||||
Reference in New Issue
Block a user