#306 RTU integration with new settings

This commit is contained in:
Mikayla Fischler 2023-11-06 09:25:44 -05:00
parent 9e13a3a467
commit 1b5e8cb69c
6 changed files with 511 additions and 341 deletions

View File

@ -156,6 +156,7 @@ local tool_ctl = {
p_assign_end = nil, ---@type graphics_element p_assign_end = nil, ---@type graphics_element
p_desc = nil, ---@type graphics_element p_desc = nil, ---@type graphics_element
p_desc_ext = nil, ---@type graphics_element p_desc_ext = nil, ---@type graphics_element
p_err = nil, ---@type graphics_element
rs_cfg_selection = nil, ---@type graphics_element rs_cfg_selection = nil, ---@type graphics_element
rs_cfg_unit_l = nil, ---@type graphics_element rs_cfg_unit_l = nil, ---@type graphics_element
@ -200,7 +201,6 @@ local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
local side_options_map = { "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 = { "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 } 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 }
local color_name_map = { [colors.red] = "red", [colors.orange] = "orange", [colors.yellow] = "yellow", [colors.lime] = "lime", [colors.green] = "green", [colors.cyan] = "cyan", [colors.lightBlue] = "lightBlue", [colors.blue] = "blue", [colors.purple] = "purple", [colors.magenta] = "magenta", [colors.pink] = "pink", [colors.white] = "white", [colors.lightGray] = "lightGray", [colors.gray] = "gray", [colors.black] = "black", [colors.brown] = "brown" }
-- convert text representation to index -- convert text representation to index
---@param side string ---@param side string
@ -223,7 +223,7 @@ local function deep_copy_peri(data)
local array = {} local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end
return array return array
end end
-- deep copy redstone defs -- deep copy redstone defs
local function deep_copy_rs(data) local function deep_copy_rs(data)
@ -236,7 +236,7 @@ end
---@param target rtu_config ---@param target rtu_config
---@param raw boolean? true to not use default values ---@param raw boolean? true to not use default values
local function load_settings(target, raw) local function load_settings(target, raw)
for _, v in pairs(fields) do settings.unset(v[1]) end for k, _ in pairs(tmp_cfg) do settings.unset(k) end
local loaded = settings.load("/rtu.settings") local loaded = settings.load("/rtu.settings")
@ -271,13 +271,14 @@ local function config_view(display)
--#region MAIN PAGE --#region MAIN PAGE
local y_start = 5 local y_start = 2
TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the RTU gateway configurator! Please select one of the following options."}
if tool_ctl.ask_config then if tool_ctl.ask_config then
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text_align=CENTER,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_align=CENTER,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)}
y_start = y_start + 5 y_start = y_start + 5
else
TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the RTU gateway configurator! Please select one of the following options."}
y_start = y_start + 3
end end
local function view_config() local function view_config()
@ -520,8 +521,11 @@ local function config_view(display)
if data ~= nil then element.set_value(data) end if data ~= nil then element.set_value(data) end
end end
local function save_and_continue() ---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections
for k, v in pairs(tmp_cfg) do settings.set(k, v) end 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
end
if settings.save("rtu.settings") then if settings.save("rtu.settings") then
load_settings(ini_cfg) load_settings(ini_cfg)
@ -537,8 +541,16 @@ local function config_view(display)
try_set(path, ini_cfg.LogPath) try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug) try_set(en_dbg, ini_cfg.LogDebug)
if not exclude_conns then
tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals)
tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone)
tool_ctl.update_peri_list()
end
tool_ctl.dev_cfg.enable() tool_ctl.dev_cfg.enable()
tool_ctl.rs_cfg.enable() tool_ctl.rs_cfg.enable()
tool_ctl.view_gw_cfg.enable()
if tool_ctl.importing_legacy then if tool_ctl.importing_legacy then
tool_ctl.importing_legacy = false tool_ctl.importing_legacy = false
@ -549,7 +561,7 @@ local function config_view(display)
PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_1,x=1,y=14,min_width=6,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.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} tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=function()save_and_continue(true)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} tool_ctl.settings_confirm = PushButton{parent=sum_c_1,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
tool_ctl.settings_confirm.hide() tool_ctl.settings_confirm.hide()
@ -648,8 +660,10 @@ local function config_view(display)
tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=51,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=51,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
PushButton{parent=peri_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_2,x=26,y=14,min_width=24,text="I don't see my device!",callback=function()peri_pane.set_value(7)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_2,x=26,y=14,min_width=24,text="I don't see my device!",callback=function()peri_pane.set_value(7)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_7,x=1,y=1,height=10,text="Make sure your device is either touching the RTU, or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."} TextBox{parent=peri_c_7,x=1,y=1,height=10,text="Make sure your device is either touching the RTU, or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
TextBox{parent=peri_c_7,x=1,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."} TextBox{parent=peri_c_7,x=1,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."}
PushButton{parent=peri_c_7,x=1,y=14,min_width=6,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_7,x=1,y=14,min_width=6,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
@ -659,6 +673,7 @@ local function config_view(display)
new_peri_attrs = { name, type } new_peri_attrs = { name, type }
tool_ctl.peri_cfg_editing = false tool_ctl.peri_cfg_editing = false
tool_ctl.p_err.hide(true)
tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':") tool_ctl.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':")
tool_ctl.p_desc_ext.set_value("") tool_ctl.p_desc_ext.set_value("")
@ -769,8 +784,6 @@ local function config_view(display)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
PushButton{parent=peri_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_3,x=1,y=1,height=4,text_align=CENTER,text="This feature is intended for advanced users. If you are clicking this just because your device is not shown, follow the connection instructions in 'I don't see my device!'."} TextBox{parent=peri_c_3,x=1,y=1,height=4,text_align=CENTER,text="This feature is intended for advanced users. If you are clicking this just because your device is not shown, follow the connection instructions in 'I don't see my device!'."}
TextBox{parent=peri_c_3,x=1,y=6,height=4,text_align=CENTER,text="Peripheral Name"} TextBox{parent=peri_c_3,x=1,y=6,height=4,text_align=CENTER,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg} local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
@ -813,8 +826,8 @@ local function config_view(display)
tool_ctl.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text_align=LEFT,text="",fg_bg=g_lg_fg_bg} tool_ctl.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text_align=LEFT,text="",fg_bg=g_lg_fg_bg}
tool_ctl.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text_align=LEFT,text="",fg_bg=g_lg_fg_bg} tool_ctl.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text_align=LEFT,text="",fg_bg=g_lg_fg_bg}
local p_err = TextBox{parent=peri_c_4,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} tool_ctl.p_err = TextBox{parent=peri_c_4,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
p_err.hide(true) tool_ctl.p_err.hide(true)
local function back_from_peri_opts() local function back_from_peri_opts()
if tool_ctl.peri_cfg_editing ~= false then if tool_ctl.peri_cfg_editing ~= false then
@ -842,33 +855,33 @@ local function config_view(display)
if (peri_type == "dynamicValve" or peri_type == "environmentDetector") and for_facility then if (peri_type == "dynamicValve" or peri_type == "environmentDetector") and for_facility then
-- skip -- skip
elseif not (util.is_int(u) and u > 0 and u < 5) then elseif not (util.is_int(u) and u > 0 and u < 5) then
p_err.set_value("Unit ID must be within 1 through 4.") tool_ctl.p_err.set_value("Unit ID must be within 1 through 4.")
p_err.show() tool_ctl.p_err.show()
return return
else unit = u end else unit = u end
end end
if peri_type == "boilerValve" then if peri_type == "boilerValve" then
if not (idx == 1 or idx == 2) then if not (idx == 1 or idx == 2) then
p_err.set_value("Index must be 1 or 2.") tool_ctl.p_err.set_value("Index must be 1 or 2.")
p_err.show() tool_ctl.p_err.show()
return return
else index = idx end else index = idx end
elseif peri_type == "turbineValve" then elseif peri_type == "turbineValve" then
if not (idx == 1 or idx == 2 or idx == 3) then if not (idx == 1 or idx == 2 or idx == 3) then
p_err.set_value("Index must be 1, 2, or 3.") tool_ctl.p_err.set_value("Index must be 1, 2, or 3.")
p_err.show() tool_ctl.p_err.show()
return return
else index = idx end else index = idx end
elseif peri_type == "dynamicValve" and not for_facility then elseif peri_type == "dynamicValve" and for_facility then
if not (util.is_int(idx) and idx > 0 and idx < 5) then if not (util.is_int(idx) and idx > 0 and idx < 5) then
p_err.set_value("Index must be within 1 through 4.") tool_ctl.p_err.set_value("Index must be within 1 through 4.")
p_err.show() tool_ctl.p_err.show()
return return
else index = idx end else index = idx end
end end
p_err.hide(true) tool_ctl.p_err.hide(true)
---@type rtu_peri_definition ---@type rtu_peri_definition
local def = { name = peri_name, unit = unit, index = index } local def = { name = peri_name, unit = unit, index = index }
@ -1124,7 +1137,7 @@ local function config_view(display)
local unit = "facility" local unit = "facility"
if def.unit then unit = "unit " .. def.unit end if def.unit then unit = "unit " .. def.unit end
if def.color ~= nil then conn = def.side .. "/" .. color_name_map[def.color] end if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local line = Div{parent=rs_import_list,height=1} local line = Div{parent=rs_import_list,height=1}
TextBox{parent=line,x=1,y=1,width=1,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=line,x=1,y=1,width=1,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
@ -1202,6 +1215,67 @@ local function config_view(display)
end end
end end
---@param def rtu_peri_definition
---@param idx integer
---@param type string
local function edit_peri_entry(idx, def, type)
-- set inputs BEFORE calling new_peri()
if def.index ~= nil then tool_ctl.p_idx.set_value(def.index) end
if def.unit == nil then
tool_ctl.p_assign_btn.set_value(1)
else
tool_ctl.p_unit.set_value(def.unit)
tool_ctl.p_assign_btn.set_value(2)
end
new_peri(def.name, type)
-- set editing mode AFTER new_peri()
tool_ctl.peri_cfg_editing = idx
end
local function delete_peri_entry(idx)
table.remove(tmp_cfg.Peripherals, idx)
tool_ctl.gen_peri_summary(tmp_cfg)
end
-- generate the peripherals summary list
---@param cfg rtu_config
function tool_ctl.gen_peri_summary(cfg)
peri_list.remove_all()
for i = 1, #cfg.Peripherals do
local def = cfg.Peripherals[i] ---@type rtu_peri_definition
local t = ppm.get_type(def.name)
local t_str = "<disconnected> (connect to edit)"
local disconnected = t == nil
if not disconnected then t_str = "[" .. t .. "]" end
local desc = " \x1a "
if type(def.index) == "number" then
desc = desc .. "#" .. def.index .. " "
end
if type(def.unit) == "number" then
desc = desc .. "for unit " .. def.unit
else
desc = desc .. "for the facility"
end
local entry = Div{parent=peri_list,height=3}
TextBox{parent=entry,x=1,y=1,height=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=entry,x=1,y=2,height=1,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=1,y=3,height=1,text=desc,fg_bg=cpair(colors.gray,colors.white)}
local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
if disconnected then edit_btn.disable() end
end
end
local function edit_rs_entry(idx) local function edit_rs_entry(idx)
local def = tmp_cfg.Redstone[idx] ---@type rtu_rs_definition local def = tmp_cfg.Redstone[idx] ---@type rtu_rs_definition
@ -1252,7 +1326,7 @@ local function config_view(display)
local conn = def.side local conn = def.side
local unit = util.strval(def.unit or "F") local unit = util.strval(def.unit or "F")
if def.color ~= nil then conn = def.side .. "/" .. color_name_map[def.color] end if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local entry = Div{parent=rs_list,height=1} local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=1,y=1,width=1,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
@ -1263,53 +1337,6 @@ local function config_view(display)
PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
end end
end end
local function edit_peri_entry(idx, name, type)
new_peri(name, type)
tool_ctl.peri_cfg_editing = idx -- must be after new_peri
end
local function delete_peri_entry(idx)
table.remove(tmp_cfg.Peripherals, idx)
tool_ctl.gen_peri_summary(tmp_cfg)
end
-- generate the peripherals summary list
---@param cfg rtu_config
function tool_ctl.gen_peri_summary(cfg)
peri_list.remove_all()
for i = 1, #cfg.Peripherals do
local def = cfg.Peripherals[i] ---@type rtu_peri_definition
local t = ppm.get_type(def.name)
local t_str = "<disconnected> (connect to edit)"
local disconnected = t == nil
if not disconnected then t_str = "[" .. t .. "]" end
local desc = " \x1a "
if type(def.index) == "number" then
desc = desc .. "#" .. def.index .. " "
end
if type(def.unit) == "number" then
desc = desc .. "for unit " .. def.unit
else
desc = desc .. "for the facility"
end
local entry = Div{parent=peri_list,height=3}
TextBox{parent=entry,x=1,y=1,height=1,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=entry,x=1,y=2,height=1,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=1,y=3,height=1,text=desc,fg_bg=cpair(colors.gray,colors.white)}
local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def.name,t)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
if disconnected then edit_btn.disable() end
end
end
end end
-- reset terminal screen -- reset terminal screen

View File

@ -109,12 +109,10 @@ local function init(panel, units)
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update) unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index) -- unit name identifier (type + index)
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.index) local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1} local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),height=1}
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
end)
-- assignment (unit # or facility) -- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)

View File

@ -5,7 +5,6 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("rtu.config")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
@ -17,6 +16,51 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
---@type rtu_config
local config = {}
rtu.config = config
-- load the RTU configuration
function rtu.load_config()
if not settings.load("/rtu.settings") then return false end
config.Peripherals = settings.get("Peripherals")
config.Redstone = settings.get("Redstone")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.SVR_Channel = settings.get("SVR_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug")
local cfv = util.new_validator()
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.RTU_Channel)
cfv.assert_type_int(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 type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey)
cfv.assert_eq(len == 0 or len >= 8, true)
end
cfv.assert_type_int(config.LogMode)
cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug)
return cfv.valid()
end
-- create a new RTU unit -- create a new RTU unit
---@nodiscard ---@nodiscard
function rtu.init_unit() function rtu.init_unit()
@ -175,7 +219,7 @@ function rtu.init_sounder(speaker)
function spkr_ctl.continue() function spkr_ctl.continue()
if spkr_ctl.playing then if spkr_ctl.playing then
if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME) local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SpeakerVolume)
if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
end end
end end
@ -203,11 +247,8 @@ end
---@nodiscard ---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param nic nic network interface device ---@param nic nic network interface device
---@param rtu_channel integer PLC comms channel
---@param svr_channel integer supervisor server channel
---@param range integer trusted device connection range
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) function rtu.comms(version, nic, conn_watchdog)
local self = { local self = {
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
seq_num = 0, seq_num = 0,
@ -218,13 +259,13 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local insert = table.insert local insert = table.insert
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
nic.closeAll() nic.closeAll()
nic.open(rtu_channel) nic.open(config.RTU_Channel)
-- send a scada management packet -- send a scada management packet
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
@ -236,7 +277,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(svr_channel, rtu_channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@ -280,7 +321,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
function public.send_modbus(m_pkt) function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(svr_channel, rtu_channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
@ -365,7 +406,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
if l_chan == rtu_channel then if l_chan == config.RTU_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() self.r_seq_num = packet.scada_frame.seq_num()

View File

@ -15,7 +15,7 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("rtu.config") local configure = require("rtu.configure")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.6.6" local RTU_VERSION = "v1.7.0"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@ -40,27 +40,26 @@ local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
---------------------------------------- ----------------------------------------
-- config validation -- get configuration
---------------------------------------- ----------------------------------------
local cfv = util.new_validator() if not rtu.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(true)
if success then
assert(rtu.load_config(), "failed to load valid RTU configuration")
else
assert(success, "RTU configuration error: " .. error)
end
end
cfv.assert_channel(config.SVR_CHANNEL) local config = rtu.config
cfv.assert_channel(config.RTU_CHANNEL)
cfv.assert_type_int(config.TRUSTED_RANGE)
cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
cfv.assert_type_table(config.RTU_DEVICES)
cfv.assert_type_table(config.RTU_REDSTONE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION) log.info("BOOTING rtu.startup " .. RTU_VERSION)
@ -85,8 +84,8 @@ local function main()
ppm.mount_all() ppm.mount_all()
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
-- get modem -- get modem
@ -139,126 +138,109 @@ local function main()
local units = __shared_memory.rtu_sys.units local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.RTU_REDSTONE local rtu_redstone = config.Redstone
local rtu_devices = config.RTU_DEVICES local rtu_devices = config.Peripherals
-- configure RTU gateway based on config file definitions -- configure RTU gateway based on settings file definitions
local function configure() local function sys_config()
-- redstone interfaces -- redstone interfaces
local rs_rtus = {}
-- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do for entry_idx = 1, #rtu_redstone do
local rs_rtu = redstone_rtu.new() local entry = rtu_redstone[entry_idx] ---@type rtu_rs_definition
local io_table = rtu_redstone[entry_idx].io ---@type table local assignment = ""
local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
-- CHECK: reactor ID must be >= to 1 if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
if (not util.is_int(io_reactor)) or (io_reactor < 0) then ---@cast for_reactor integer
local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0") assignment = "reactor unit " .. entry.unit
if rs_rtus[for_reactor] == nil then
log.debug(util.c("configure> allocated redstone RTU for reactor unit ", entry.unit))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
elseif entry.unit == nil then
assignment = "facility"
for_reactor = 0
if rs_rtus[for_reactor] == nil then
log.debug(util.c("configure> allocated redstone RTU for the facility"))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
else
local message = util.c("configure> invalid unit assignment at block index #", entry_idx)
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
end end
-- CHECK: io table exists
if type(io_table) ~= "table" then
local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
println(message)
log.fatal(message)
return false
end
local capabilities = {}
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
local continue = true
-- CHECK: no duplicate entries
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
-- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O")
println(message)
log.warning(message)
continue = false
break
end
end
-- not a duplicate
if continue then
for i = 1, #io_table do
local valid = false
local conf = io_table[i]
-- verify configuration -- verify configuration
if rsio.is_valid_port(conf.port) and rsio.is_valid_side(conf.side) then local valid = false
if conf.bundled_color then if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
valid = rsio.is_color(conf.bundled_color) valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
else
valid = true
end
end end
local rs_rtu = rs_rtus[for_reactor].rtu
local capabilities = rs_rtus[for_reactor].capabilities
if not valid then if not valid then
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx, local message = util.c("configure> invalid redstone definition at block index #", entry_idx)
" (for reactor ", io_reactor, ")")
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
else else
-- link redstone in RTU -- link redstone in RTU
local mode = rsio.get_io_mode(conf.port) local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs -- can't have duplicate inputs
if util.table_contains(capabilities, conf.port) then if util.table_contains(capabilities, entry.port) then
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side) local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
println(message) println(message)
log.warning(message) log.warning(message)
else else
rs_rtu.link_di(conf.side, conf.bundled_color) rs_rtu.link_di(entry.side, entry.color)
end end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(conf.side, conf.bundled_color) rs_rtu.link_do(entry.side, entry.color)
elseif mode == rsio.IO_MODE.ANALOG_IN then elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs -- can't have duplicate inputs
if util.table_contains(capabilities, conf.port) then if util.table_contains(capabilities, entry.port) then
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side) local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
println(message) println(message)
log.warning(message) log.warning(message)
else else
rs_rtu.link_ai(conf.side) rs_rtu.link_ai(entry.side)
end end
elseif mode == rsio.IO_MODE.ANALOG_OUT then elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(conf.side) rs_rtu.link_ao(entry.side)
else else
-- should be unreachable code, we already validated ports -- should be unreachable code, we already validated ports
log.error("configure> fell through if chain attempting to identify IO mode", true) log.error("configure> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true)
println("configure> encountered a software error, check logs") println("configure> encountered a software error, check logs")
return false return false
end end
table.insert(capabilities, conf.port) table.insert(capabilities, entry.port)
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.port), log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
" (", conf.side, ") for reactor ", io_reactor))
end end
end end
-- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do
---@class rtu_unit_registry_entry ---@class rtu_unit_registry_entry
local unit = { local unit = {
uid = 0, ---@type integer uid = 0, ---@type integer
name = "redstone_io", ---@type string name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = entry_idx, ---@type integer index = false, ---@type integer|false
reactor = io_reactor, ---@type integer reactor = for_reactor, ---@type integer
device = capabilities, ---@type table use device field for redstone ports device = def.capabilities, ---@type table use device field for redstone ports
is_multiblock = false, ---@type boolean is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil formed = nil, ---@type boolean|nil
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device rtu = def.rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rs_rtu, false), modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil thread = nil ---@type parallel_thread|nil
} }
@ -266,8 +248,8 @@ local function main()
table.insert(units, unit) table.insert(units, unit)
local for_message = "facility" local for_message = "facility"
if io_reactor > 0 then if util.is_int(for_reactor) then
for_message = util.c("reactor ", io_reactor) for_message = util.c("reactor unit ", for_reactor)
end end
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message)) log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
@ -276,13 +258,13 @@ local function main()
databus.tx_unit_hw_status(unit.uid, unit.hw_state) databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end end
end
-- mounted peripherals -- mounted peripherals
for i = 1, #rtu_devices do for i = 1, #rtu_devices do
local name = rtu_devices[i].name local entry = rtu_devices[i] ---@type rtu_peri_definition
local index = rtu_devices[i].index local name = entry.name
local for_reactor = rtu_devices[i].for_reactor local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string -- CHECK: name is a string
if type(name) ~= "string" then if type(name) ~= "string" then
@ -292,20 +274,37 @@ local function main()
return false return false
end end
-- CHECK: index is an integer >= 1 -- CHECK: index type
if (not util.is_int(index)) or (index <= 0) then if (index ~= nil) and (not util.is_int(index)) then
local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1") local message = util.c("configure> device entry #", i, ": index ", index, " isn't valid")
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
end end
-- CHECK: reactor is an integer >= 0 -- CHECK: index range
if (not util.is_int(for_reactor)) or (for_reactor < 0) then local function validate_index(min, max)
local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0") if (util.is_int(index) and index < min) and (max == nil or index > max) then
local message = util.c("configure> device entry #", i, ": index ", index, " isn't >= ", min, " and <= ", max)
println(message) println(message)
log.fatal(message) log.fatal(message)
return false return false
else return true end
end
-- CHECK: reactor is an integer >= 0
local function validate_assign(for_facility)
if for_facility and for_reactor ~= 0 then
local message = util.c("configure> device entry #", i, ": must only be for the facility")
println(message)
log.fatal(message)
return false
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
local message = util.c("configure> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
println(message)
log.fatal(message)
return false
else return true end
end end
local device = ppm.get_periph(name) local device = ppm.get_periph(name)
@ -330,6 +329,9 @@ local function main()
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
if not validate_index(1, 2) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface, faulted = boilerv_rtu.new(device) rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true is_multiblock = true
@ -342,6 +344,9 @@ local function main()
end end
elseif type == "turbineValve" then elseif type == "turbineValve" then
-- turbine multiblock -- turbine multiblock
if not validate_index(1, 3) then return false end
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface, faulted = turbinev_rtu.new(device) rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true is_multiblock = true
@ -354,6 +359,14 @@ local function main()
end end
elseif type == "dynamicValve" then elseif type == "dynamicValve" then
-- dynamic tank multiblock -- dynamic tank multiblock
if entry.unit == nil then
if not validate_index(1, 4) then return false end
if not validate_assign(true) then return false end
else
if not validate_index(1, 1) then return false end
if not validate_assign() then return false end
end
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
rtu_iface, faulted = dynamicv_rtu.new(device) rtu_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true is_multiblock = true
@ -366,6 +379,8 @@ local function main()
end end
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device) rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true is_multiblock = true
@ -378,6 +393,8 @@ local function main()
end end
elseif type == "spsPort" then elseif type == "spsPort" then
-- SPS multiblock -- SPS multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device) rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true is_multiblock = true
@ -390,10 +407,14 @@ local function main()
end end
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
-- SNA -- SNA
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device) rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device) rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then elseif type == ppm.VIRTUAL_DEVICE_TYPE then
@ -423,7 +444,7 @@ local function main()
uid = 0, ---@type integer uid = 0, ---@type integer
name = name, ---@type string name = name, ---@type string
type = rtu_type, ---@type RTU_UNIT_TYPE type = rtu_type, ---@type RTU_UNIT_TYPE
index = index, ---@type integer index = index or false, ---@type integer|false
reactor = for_reactor, ---@type integer reactor = for_reactor, ---@type integer
device = device, ---@type table device = device, ---@type table
is_multiblock = is_multiblock, ---@type boolean is_multiblock = is_multiblock, ---@type boolean
@ -465,7 +486,6 @@ local function main()
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end end
-- we made it through all that trusting-user-to-write-a-config-file chaos
return true return true
end end
@ -475,9 +495,9 @@ local function main()
local rtu_state = __shared_memory.rtu_state local rtu_state = __shared_memory.rtu_state
log.debug("boot> running configure()") log.debug("boot> running sys_config()")
if configure() then if sys_config() then
-- start UI -- start UI
local message local message
rtu_state.fp_ok, message = renderer.try_start_ui(units) rtu_state.fp_ok, message = renderer.try_start_ui(units)
@ -502,12 +522,11 @@ local function main()
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders) databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, config.RTU_CHANNEL, config.SVR_CHANNEL, smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
-- init threads -- init threads

View File

@ -28,6 +28,174 @@ local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
---@param smem rtu_shared_memory
---@param println_ts function
---@param iface string
---@param type string
---@param device table
---@param unit rtu_unit_registry_entry
local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
local sys = smem.rtu_sys
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == iface then
local resend_advert = false
local faulted = false
local unknown = false
local invalid = false
-- found, re-link
unit.device = device
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
if unit.reactor < 1 or unit.reactor > 4 then
invalid = true
log.error(util.c("boiler '", unit.name, "' cannot init, not assigned to a valid unit in config"))
end
if (unit.index == false) or unit.index < 1 or unit.index > 2 then
invalid = true
log.error(util.c("boiler '", unit.name, "' cannot init, invalid index provided in config"))
end
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
if unit.reactor < 1 or unit.reactor > 4 then
invalid = true
log.error(util.c("turbine '", unit.name, "' cannot init, not assigned to a valid unit in config"))
end
if (unit.index == false) or unit.index < 1 or unit.index > 3 then
invalid = true
log.error(util.c("turbine '", unit.name, "' cannot init, invalid index provided in config"))
end
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "dynamicValve" then
-- dynamic tank multiblock
if unit.reactor < 0 or unit.reactor > 4 then
invalid = true
log.error(util.c("dynamic tank '", unit.name, "' cannot init, no valid assignment provided in config"))
end
if (unit.reactor == 0 and ((unit.index == false) or unit.index < 1 or unit.index > 4)) or
(unit.reactor > 0 and unit.index ~= 1) then
invalid = true
log.error(util.c("dynamic tank '", unit.name, "' cannot init, invalid index provided in config"))
end
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
if unit.reactor ~= 0 then
invalid = true
log.error(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility in config"))
end
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
if unit.reactor ~= 0 then
invalid = true
log.error(util.c("SPS '", unit.name, "' cannot init, not assigned to facility in config"))
end
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
if unit.reactor < 1 or unit.reactor > 4 then
invalid = true
log.error(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit in config"))
end
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
if unit.reactor < 0 or unit.reactor > 4 then
invalid = true
log.error(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided in config"))
end
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
databus.tx_unit_hw_type(unit.uid, unit.type)
end
-- if disconnected on startup, config wouldn't have been validated
-- checking now that it has connected; the config isn't valid, so don't connect it
if invalid then
unit.hw_state = UNIT_HW_STATE.OFFLINE
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
return
end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end
if unit.is_multiblock then
unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
elseif faulted then
unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else
unit.hw_state = UNIT_HW_STATE.OFFLINE
end
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
if not unknown then
unit.modbus_io = modbus.new(unit.rtu, true)
local type_name = types.rtu_type_to_string(unit.type)
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then
sys.rtu_comms.send_advertisement(sys.units)
else
sys.rtu_comms.send_remounted(unit.uid)
end
end
end
end
-- main thread -- main thread
---@nodiscard ---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
@ -180,102 +348,7 @@ function threads.thread__main(smem)
else else
-- relink lost peripheral to correct unit entry -- relink lost peripheral to correct unit entry
for i = 1, #units do for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry handle_unit_mount(smem, println_ts, param1, type, device, units[i])
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then
local resend_advert = false
local faulted = false
local unknown = false
-- found, re-link
unit.device = device
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
databus.tx_unit_hw_type(unit.uid, unit.type)
end
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit.rtu, faulted = dynamicv_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end
if unit.is_multiblock then
unit.hw_state = UNIT_HW_STATE.UNFORMED
if unit.formed == false then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
elseif faulted then
unit.hw_state = UNIT_HW_STATE.FAULTED
elseif not unknown then
unit.hw_state = UNIT_HW_STATE.OK
else
unit.hw_state = UNIT_HW_STATE.OFFLINE
end
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
if not unknown then
unit.modbus_io = modbus.new(unit.rtu, true)
local type_name = types.rtu_type_to_string(unit.type)
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then
rtu_comms.send_advertisement(units)
else
rtu_comms.send_remounted(unit.uid)
end
end
end
end end
end end
end end

View File

@ -285,6 +285,18 @@ function rsio.is_color(color)
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0) return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0)
end end
-- color to string
---@nodiscard
---@param color color
---@return string
function rsio.color_name(color)
local color_name_map = { [colors.red] = "red", [colors.orange] = "orange", [colors.yellow] = "yellow", [colors.lime] = "lime", [colors.green] = "green", [colors.cyan] = "cyan", [colors.lightBlue] = "lightBlue", [colors.blue] = "blue", [colors.purple] = "purple", [colors.magenta] = "magenta", [colors.pink] = "pink", [colors.white] = "white", [colors.lightGray] = "lightGray", [colors.gray] = "gray", [colors.black] = "black", [colors.brown] = "brown" }
if rsio.is_color(color) then
return color_name_map[color]
else return "unknown" end
end
--#endregion --#endregion
--#region Digital I/O --#region Digital I/O