Compare commits
62 Commits
v1.8.18-be
...
v1.8.20-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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/manifest.yml
vendored
2
.github/workflows/manifest.yml
vendored
@@ -46,7 +46,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
|
||||
|
||||
@@ -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")
|
||||
@@ -46,7 +46,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 +71,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 +120,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 +153,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 +336,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 +762,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 +959,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 +979,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 +1134,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 +1368,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
|
||||
|
||||
@@ -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)
|
||||
@@ -517,7 +520,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?")
|
||||
@@ -702,7 +705,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,
|
||||
|
||||
@@ -126,92 +144,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
|
||||
@@ -692,7 +624,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
|
||||
|
||||
@@ -70,8 +70,8 @@ function process.init(iocontrol, coord_comms)
|
||||
|
||||
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_charge_target", self.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
self.io.facility.ps.publish("process_gen_target", self.io.energy_convert_from_fe(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)
|
||||
@@ -316,8 +316,8 @@ function process.start_ack_handle(response)
|
||||
|
||||
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_charge_target", self.io.energy_convert_from_fe(ctl_proc.charge_target))
|
||||
self.io.facility.ps.publish("process_gen_target", self.io.energy_convert_from_fe(ctl_proc.gen_target))
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
end
|
||||
|
||||
@@ -106,7 +106,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_header .. "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
|
||||
@@ -186,6 +186,10 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
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_header .. "terminated session due to an unexpected ESTABLISH packet")
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
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.6"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
|
||||
@@ -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,8 +56,10 @@ 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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.3"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,7 +33,8 @@ 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 +77,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 +96,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 +178,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 +379,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 +516,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
|
||||
|
||||
@@ -10,6 +10,9 @@ local util = require("scada-common.util")
|
||||
|
||||
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 +38,15 @@ local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
local config = nil ---@type pkt_config
|
||||
|
||||
-- initialize facility-independent components of pocket iocontrol
|
||||
---@param comms pocket_comms
|
||||
---@param nav pocket_nav
|
||||
function iocontrol.init_core(comms, nav)
|
||||
---@param cfg pkt_config
|
||||
function iocontrol.init_core(comms, nav, cfg)
|
||||
config = cfg
|
||||
|
||||
io.nav = nav
|
||||
|
||||
---@class pocket_ioctl_diag
|
||||
@@ -86,10 +94,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 +112,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 = {
|
||||
@@ -329,8 +350,8 @@ 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 +363,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)
|
||||
|
||||
@@ -36,6 +36,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 +53,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)
|
||||
@@ -490,7 +493,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 +506,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 +514,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
|
||||
|
||||
@@ -606,7 +610,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 ..
|
||||
@@ -675,17 +679,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 +699,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 +737,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 +833,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
|
||||
|
||||
@@ -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.11.8-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
|
||||
|
||||
@@ -144,7 +144,7 @@ local function new_view(root)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,7 +42,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)}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ return function (data, base_page, title, items, scroll_height)
|
||||
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}
|
||||
PushButton{parent=name_list,text=item.name,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
|
||||
|
||||
if i % 12 == 0 then util.nop() end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
621
reactor-plc/config/system.lua
Normal file
621
reactor-plc/config/system.lua
Normal file
@@ -0,0 +1,621 @@
|
||||
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 LEFT = core.ALIGN.LEFT
|
||||
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,alignment=LEFT}
|
||||
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()
|
||||
@@ -827,7 +833,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.6"
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -444,7 +444,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 +571,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.6"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
|
||||
@@ -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,
|
||||
@@ -271,8 +300,7 @@ types.ALARM_STATE_NAMES = {
|
||||
|
||||
--#endregion
|
||||
|
||||
-- STRING TYPES --
|
||||
--#region
|
||||
--#region STRING TYPES
|
||||
|
||||
---@alias side
|
||||
---|"top"
|
||||
@@ -386,8 +414,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.3"
|
||||
|
||||
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
|
||||
|
||||
@@ -181,7 +181,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 +453,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 +470,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 +907,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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -53,7 +53,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 +184,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 +205,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 +213,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
|
||||
@@ -252,7 +256,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 +265,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
|
||||
@@ -307,13 +311,13 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
if pkt.length == 3 then
|
||||
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
|
||||
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
|
||||
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 +326,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
|
||||
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 +374,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 +441,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 +456,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,7 +393,7 @@ 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
|
||||
@@ -408,7 +408,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
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 +424,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 +433,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 +449,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 +459,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 +472,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 +485,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 +498,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 +506,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 +530,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 +538,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 +643,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 +700,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 +749,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 +764,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 +806,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.2"
|
||||
|
||||
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