cc-mek-scada/rtu/config/peripherals.lua
2025-09-12 19:45:15 -04:00

439 lines
21 KiB
Lua

local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local core = require("graphics.core")
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 PushButton = require("graphics.elements.controls.PushButton")
local Radio2D = require("graphics.elements.controls.Radio2D")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local TextField = require("graphics.elements.form.TextField")
---@class rtu_peri_definition
---@field unit integer|nil
---@field index integer|nil
---@field name string
local tri = util.trinary
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local self = {
peri_cfg_editing = false, ---@type integer|false
p_assign = nil, ---@type function
ppm_devs = nil, ---@type ListBox
p_name_msg = nil, ---@type TextBox
p_prompt = nil, ---@type TextBox
p_idx = nil, ---@type NumberField
p_unit = nil, ---@type NumberField
p_desc = nil, ---@type TextBox
p_desc_ext = nil, ---@type TextBox
p_err = nil ---@type TextBox
}
local peripherals = {}
local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "reinforcedInductionPort", "spsPort", "solarNeutronActivator", "environmentDetector", "environment_detector" }
local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector", "environment_detector" }
-- create the peripherals configuration view
---@param tool_ctl _rtu_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ rtu_config, rtu_config, rtu_config, table, function ]
---@param peri_cfg Div
---@param style { [string]: cpair }
---@return MultiPane peri_pane, string[] NEEDS_UNIT
function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
local settings_cfg, ini_cfg, tmp_cfg, _, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
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 Peripherals
local peri_c_1 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_c_2 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_c_3 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_c_4 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_c_5 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_c_6 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_c_7 = Div{parent=peri_cfg,x=2,y=4,width=49}
local peri_pane = MultiPane{parent=peri_cfg,x=1,y=4,panes={peri_c_1,peri_c_2,peri_c_3,peri_c_4,peri_c_5,peri_c_6,peri_c_7}}
TextBox{parent=peri_cfg,x=1,y=2,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)}
local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function peri_revert()
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tool_ctl.gen_peri_summary()
end
local function peri_apply()
settings.set("Peripherals", tmp_cfg.Peripherals)
if settings.save("/rtu.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
peri_pane.set_value(5)
-- for return to list from saved screen
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tool_ctl.gen_peri_summary()
else
peri_pane.set_value(6)
end
end
PushButton{parent=peri_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local peri_revert_btn = PushButton{parent=peri_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=peri_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=peri_c_1,x=35,y=14,min_width=7,text="Add +",callback=function()peri_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local peri_apply_btn = PushButton{parent=peri_c_1,x=43,y=14,min_width=7,text="Apply",callback=peri_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
TextBox{parent=peri_c_2,x=1,y=1,text="Select one of the below devices to use."}
self.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
PushButton{parent=peri_c_2,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_2,x=26,y=14,min_width=24,text="I don't see my device!",callback=function()peri_pane.set_value(7)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_7,x=1,y=1,height=10,text="Make sure your device is either touching the RTU or connected via wired modems. There should be a wired modem on a side of the RTU then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
TextBox{parent=peri_c_7,x=1,y=9,height=4,text="If it still does not show, it may not be compatible. Currently only Boilers, Turbines, Dynamic Tanks, SNAs, SPSs, Induction Matricies, and Environment Detectors are supported."}
PushButton{parent=peri_c_7,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local new_peri_attrs = { "", "" }
local function new_peri(name, type)
new_peri_attrs = { name, type }
self.peri_cfg_editing = false
self.p_err.hide(true)
self.p_name_msg.set_value("Configuring peripheral on '" .. name .. "':")
self.p_desc_ext.set_value("")
local function reposition(prompt, idx_x, idx_max, unit_x, unit_y, desc_y)
self.p_prompt.set_value(prompt)
self.p_idx.reposition(idx_x, 4)
self.p_idx.enable()
self.p_idx.set_max(idx_max)
self.p_idx.show()
self.p_unit.reposition(unit_x, unit_y)
self.p_unit.enable()
self.p_unit.show()
self.p_desc.reposition(1, desc_y)
end
if type == "boilerValve" then
reposition("This is reactor unit # 's # boiler.", 31, 2, 23, 4, 7)
self.p_assign_btn.hide(true)
self.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
reposition("This is reactor unit # 's # turbine.", 31, 3, 23, 4, 7)
self.p_assign_btn.hide(true)
self.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)
self.p_idx.hide()
self.p_assign_btn.hide(true)
self.p_desc_ext.set_value("Warning: too many devices on one RTU Gateway can cause lag. Note that 10x the \"PEAK\x1a\" rate on the flow monitor gives you the mB/t of waste that the SNA(s) can process. Enough SNAs to provide 2x to 3x of that unit's max burn rate should be a good margin to catch up after night or cloudy weather.")
elseif type == "dynamicValve" then
reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8)
self.p_assign_btn.show()
self.p_assign_btn.redraw()
if self.p_assign_btn.get_value() == 1 then
self.p_idx.enable()
self.p_unit.disable()
else
self.p_idx.set_value(1)
self.p_idx.disable()
self.p_unit.enable()
end
self.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" or type == "environment_detector" then
reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8)
self.p_assign_btn.show()
self.p_assign_btn.redraw()
if self.p_assign_btn.get_value() == 1 then self.p_unit.disable() else self.p_unit.enable() end
self.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 == "reinforcedInductionPort" or type == "spsPort" then
local dev = tri(type == "inductionPort" or type == "reinforcedInductionPort", "induction matrix", "SPS")
self.p_idx.hide(true)
self.p_unit.hide(true)
self.p_prompt.set_value("This is the " .. dev .. " for the facility.")
self.p_assign_btn.hide(true)
self.p_desc.reposition(1, 7)
self.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
assert(false, "invalid peripheral type after type validation")
end
peri_pane.set_value(4)
end
-- update peripherals list
function tool_ctl.update_peri_list()
local alternate = true
local mounts = ppm.list_mounts()
-- filter out in-use peripherals
for _, v in ipairs(tmp_cfg.Peripherals) do mounts[v.name] = nil end
self.ppm_devs.remove_all()
for name, entry in pairs(mounts) do
if util.table_contains(RTU_DEV_TYPES, entry.type) then
local bkg = tri(alternate, colors.white, colors.lightGray)
---@cast entry ppm_entry
local line = Div{parent=self.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)}
PushButton{parent=line,x=1,y=1,min_width=9,alignment=LEFT,height=1,text="> SELECT",callback=function()new_peri(name,entry.type)end,fg_bg=cpair(colors.black,colors.purple),active_fg_bg=cpair(colors.white,colors.black)}
TextBox{parent=line,x=11,y=1,text=name,fg_bg=cpair(colors.black,bkg)}
TextBox{parent=line,x=11,y=2,text=entry.type,fg_bg=cpair(colors.gray,bkg)}
alternate = not alternate
end
end
end
tool_ctl.update_peri_list()
TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you just can't see your device, click 'I don't see my device!' instead."}
TextBox{parent=peri_c_3,x=1,y=5,height=4,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=6,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
local p_type = Radio2D{parent=peri_c_3,x=1,y=8,rows=5,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
man_p_err.hide(true)
local function submit_manual_peri()
local name = p_name.get_value()
if string.len(name) > 0 then
tool_ctl.entering_manual = true
man_p_err.hide(true)
new_peri(name, RTU_DEV_TYPES[p_type.get_value()])
else man_p_err.show() end
end
PushButton{parent=peri_c_3,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_3,x=44,y=14,text="Next \x1a",callback=submit_manual_peri,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.p_name_msg = TextBox{parent=peri_c_4,x=1,y=1,height=2,text=""}
self.p_prompt = TextBox{parent=peri_c_4,x=1,y=4,height=2,text=""}
self.p_idx = NumberField{parent=peri_c_4,x=31,y=4,width=4,max_chars=2,min=1,max=2,default=1,fg_bg=bw_fg_bg,dis_fg_bg=btn_dis_fg_bg}
self.p_assign_btn = RadioButton{parent=peri_c_4,x=1,y=5,default=1,options={"the facility","reactor unit #"},callback=function(v)self.p_assign(v)end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
self.p_unit = NumberField{parent=peri_c_4,x=23,y=4,width=4,max_chars=2,min=1,max=4,default=1,fg_bg=bw_fg_bg,dis_fg_bg=btn_dis_fg_bg}
self.p_unit.disable()
function self.p_assign(opt)
if opt == 1 then
self.p_unit.disable()
if new_peri_attrs[2] == "dynamicValve" then self.p_idx.enable() end
else
self.p_unit.enable()
if new_peri_attrs[2] == "dynamicValve" then
self.p_idx.set_value(1)
self.p_idx.disable()
end
end
end
self.p_desc = TextBox{parent=peri_c_4,x=1,y=7,height=6,text="",fg_bg=g_lg_fg_bg}
self.p_desc_ext = TextBox{parent=peri_c_4,x=1,y=6,height=7,text="",fg_bg=g_lg_fg_bg}
self.p_err = TextBox{parent=peri_c_4,x=8,y=14,width=32,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
self.p_err.hide(true)
local function back_from_peri_opts()
if self.peri_cfg_editing ~= false then
peri_pane.set_value(1)
elseif tool_ctl.entering_manual then
peri_pane.set_value(3)
else
peri_pane.set_value(2)
end
tool_ctl.entering_manual = false
end
local function save_peri_entry()
local peri_name = new_peri_attrs[1]
local peri_type = new_peri_attrs[2]
local unit, index = nil, nil
local for_facility = self.p_assign_btn.get_value() == 1
local u = tonumber(self.p_unit.get_value())
local idx = tonumber(self.p_idx.get_value())
if util.table_contains(NEEDS_UNIT, peri_type) then
if (peri_type == "dynamicValve" or peri_type == "environmentDetector" or peri_type == "environment_detector") and for_facility then
-- skip
elseif not (util.is_int(u) and u > 0 and u < 5) then
self.p_err.set_value("Unit ID must be within 1 to 4.")
self.p_err.show()
return
else unit = u end
end
if peri_type == "boilerValve" then
if not (idx == 1 or idx == 2) then
self.p_err.set_value("Index must be 1 or 2.")
self.p_err.show()
return
else index = idx end
elseif peri_type == "turbineValve" then
if not (idx == 1 or idx == 2 or idx == 3) then
self.p_err.set_value("Index must be 1, 2, or 3.")
self.p_err.show()
return
else index = idx end
elseif peri_type == "dynamicValve" and for_facility then
if not (util.is_int(idx) and idx > 0 and idx < 5) then
self.p_err.set_value("Index must be within 1 to 4.")
self.p_err.show()
return
else index = idx end
elseif peri_type == "dynamicValve" then
index = 1
elseif peri_type == "environmentDetector" or peri_type == "environment_detector" then
if not (util.is_int(idx) and idx > 0) then
self.p_err.set_value("Index must be greater than 0.")
self.p_err.show()
return
else index = idx end
end
self.p_err.hide(true)
---@type rtu_peri_definition
local def = { name = peri_name, unit = unit, index = index }
if self.peri_cfg_editing == false then
table.insert(tmp_cfg.Peripherals, def)
else
def.name = tmp_cfg.Peripherals[self.peri_cfg_editing].name
tmp_cfg.Peripherals[self.peri_cfg_editing] = def
end
peri_pane.set_value(1)
tool_ctl.gen_peri_summary()
tool_ctl.update_peri_list()
self.p_idx.set_value(1)
end
PushButton{parent=peri_c_4,x=1,y=14,text="\x1b Back",callback=back_from_peri_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_peri_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_5,x=1,y=1,text="Settings saved!"}
PushButton{parent=peri_c_5,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=peri_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=peri_c_6,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=peri_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Tool Functions
---@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 self.p_idx.set_value(def.index) end
if def.unit == nil then
self.p_assign_btn.set_value(1)
else
self.p_unit.set_value(def.unit)
self.p_assign_btn.set_value(2)
end
new_peri(def.name, type)
-- set editing mode AFTER new_peri()
self.peri_cfg_editing = idx
end
local function delete_peri_entry(idx)
table.remove(tmp_cfg.Peripherals, idx)
tool_ctl.gen_peri_summary()
tool_ctl.update_peri_list()
end
-- generate the peripherals summary list
function tool_ctl.gen_peri_summary()
peri_list.remove_all()
local modified = #ini_cfg.Peripherals ~= #tmp_cfg.Peripherals
for i = 1, #tmp_cfg.Peripherals do
local def = tmp_cfg.Peripherals[i]
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,text="@ "..def.name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=entry,x=1,y=2,text=" \x1a "..t_str,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=1,y=3,text=desc,fg_bg=cpair(colors.gray,colors.white)}
local edit_btn = PushButton{parent=entry,x=41,y=2,min_width=8,height=1,text="EDIT",callback=function()edit_peri_entry(i,def,t or "")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=entry,x=41,y=3,min_width=8,height=1,text="DELETE",callback=function()delete_peri_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
if disconnected then edit_btn.disable() end
if not modified then
local a = ini_cfg.Peripherals[i]
local b = tmp_cfg.Peripherals[i]
modified = (a.unit ~= b.unit) or (a.index ~= b.index) or (a.name ~= b.name)
end
end
if modified then
peri_revert_btn.enable()
peri_apply_btn.enable()
else
peri_revert_btn.disable()
peri_apply_btn.disable()
end
end
--#endregion
return peri_pane, NEEDS_UNIT
end
return peripherals