Merge branch 'devel' into 580-wired-comms-networking

This commit is contained in:
Mikayla
2025-05-19 13:26:36 +00:00
27 changed files with 890 additions and 539 deletions

View File

@@ -136,22 +136,45 @@ local function self_check()
self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones")
-- check redstone configurations
local ifaces = {}
local bundled_sides = {}
local phys = {} ---@type rtu_rs_definition[][]
local inputs = { [0] = {}, {}, {}, {}, {} }
for i = 1, #cfg.Redstone do
local entry = cfg.Redstone[i]
local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "")
local dupe = util.table_contains(ifaces, ident)
local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil))
local name = entry.relay or "local"
local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is")
if phys[name] == nil then phys[name] = {} end
table.insert(phys[entry.relay or "local"], entry)
end
self.self_check_msg("> check redstone " .. ident .. " unique...", not dupe, "only one port should be set to a side/color combination")
self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work")
self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry")
for name, entries in pairs(phys) do
TextBox{parent=self.sc_log,text="> checking redstone @ "..name.."...",fg_bg=cpair(colors.blue,colors.white)}
bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil
table.insert(ifaces, ident)
local ifaces = {}
local bundled_sides = {}
for i = 1, #entries do
local entry = entries[i]
local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "")
local sc_dupe = util.table_contains(ifaces, ident)
local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil))
local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is")
self.self_check_msg("> check redstone " .. ident .. " unique...", not sc_dupe, "only one port should be set to a side/color combination")
self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work")
self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry")
if rsio.get_io_dir(entry.port) == rsio.IO_DIR.IN then
local in_dupe = util.table_contains(inputs[entry.unit or 0], entry.port)
self.self_check_msg("> check redstone " .. ident .. " input...", not in_dupe, "you cannot have multiple of the same input for a given unit or the facility ("..rsio.to_string(entry.port)..")")
end
bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil
table.insert(ifaces, ident)
end
end
-- check peripheral configurations
@@ -245,7 +268,7 @@ function check.create(main_pane, settings_cfg, check_sys, style)
TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg}
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=500,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
self.sc_log = ListBox{parent=sc,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 last_check = { nil, nil }

View File

@@ -1,4 +1,5 @@
local constants = require("scada-common.constants")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
@@ -18,6 +19,7 @@ local NumberField = require("graphics.elements.form.NumberField")
---@class rtu_rs_definition
---@field unit integer|nil
---@field port IO_PORT
---@field relay string|nil
---@field side side
---@field color color|nil
---@field invert true|nil
@@ -33,6 +35,7 @@ local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT
local self = {
rs_cfg_phy = false, ---@type string|nil|false
rs_cfg_port = 1, ---@type IO_PORT
rs_cfg_editing = false, ---@type integer|false
@@ -108,6 +111,23 @@ local function color_to_idx(color)
end
end
-- select the subset of redstone entries assigned to the given phy
---@param cfg rtu_rs_definition[] the full redstone entry list
---@param phy string|nil which phy to get redstone entries for
---@param invert boolean? true to get all except this phy
---@return rtu_rs_definition[]
local function redstone_subset(cfg, phy, invert)
local subset = {}
for i = 1, #cfg do
if ((not invert) and cfg[i].relay == phy) or (invert and cfg[i].relay ~= phy) then
table.insert(subset, cfg[i])
end
end
return subset
end
local redstone = {}
-- validate a redstone entry
@@ -137,21 +157,89 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
--#region Redstone
local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_9 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_10 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8}}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8,rs_c_9,rs_c_10}}
TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
local header = TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
--#region Interface Selection
TextBox{parent=rs_c_1,x=1,y=1,text="Configure this computer or a redstone relay."}
local iface_list = ListBox{parent=rs_c_1,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
-- update relay interface list
function tool_ctl.update_relay_list()
local mounts = ppm.list_mounts()
iface_list.remove_all()
-- assemble list of configured relays
local relays = {}
for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i]
if def.relay and not util.table_contains(relays, def.relay) then
table.insert(relays, def.relay)
end
end
-- add unconfigured connected relays
for name, entry in pairs(mounts) do
if entry.type == "redstone_relay" and not util.table_contains(relays, name) then
table.insert(relays, name)
end
end
local function config_rs(name)
header.set_value(" Redstone Connections (" .. name .. ")")
self.rs_cfg_phy = tri(name == "local", nil, name)
tool_ctl.gen_rs_summary()
rs_pane.set_value(2)
end
local line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="This Computer",fg_bg=cpair(colors.gray,colors.white)}
local count = #redstone_subset(ini_cfg.Redstone, nil)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs("local")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
for i = 1, #relays do
local name = relays[i]
line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="Redstone Relay",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=line,x=18,y=2,text=tri(mounts[name],"ONLINE","OFFLINE"),fg_bg=cpair(tri(mounts[name],colors.green,colors.red),colors.white)}
count = #redstone_subset(ini_cfg.Redstone, name)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
end
tool_ctl.update_relay_list()
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_1,x=27,y=14,min_width=23,text="I don't see my relay!",callback=function()rs_pane.set_value(10)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Configuration List
TextBox{parent=rs_c_2,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_2,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function rs_revert()
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
@@ -159,40 +247,47 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
end
local function rs_apply()
settings.set("Redstone", tmp_cfg.Redstone)
-- add the changed data to the existing saved data
local new_data = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local new_save = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy, true)
for i = 1, #new_data do table.insert(new_save, new_data[i]) end
settings.set("Redstone", new_save)
if settings.save("/rtu.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
rs_pane.set_value(4)
rs_pane.set_value(5)
-- for return to list from saved screen
-- this will delete unsaved changes for other phy's, which is acceptable
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
tool_ctl.gen_rs_summary()
tool_ctl.update_relay_list()
else
rs_pane.set_value(5)
rs_pane.set_value(6)
end
end
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local rs_revert_btn = PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function rs_back()
self.rs_cfg_phy = false
rs_pane.set_value(1)
header.set_value(" Redstone Connections")
end
TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."}
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local rs_revert_btn = PushButton{parent=rs_c_2,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_2,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local rs_apply_btn = PushButton{parent=rs_c_2,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
--#endregion
--#region Port Selection
TextBox{parent=rs_c_3,x=1,y=1,text="Select one of the below ports to use."}
local rs_ports = ListBox{parent=rs_c_3,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function new_rs(port)
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port then
rs_pane.set_value(6)
return
end
end
end
self.rs_cfg_editing = false
local text
@@ -249,7 +344,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_selection.set_value(text)
self.rs_cfg_port = port
rs_pane.set_value(3)
rs_pane.set_value(4)
end
-- add entries to redstone option list
@@ -270,38 +365,43 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""}
--#endregion
--#region Port Configuration
PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_selection = TextBox{parent=rs_c_4,x=1,y=1,height=2,text=""}
self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"}
local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
PushButton{parent=rs_c_4,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"}
self.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
self.rs_cfg_side_l = TextBox{parent=rs_c_4,x=1,y=4,width=11,text="Output Side"}
local side = Radio2D{parent=rs_c_4,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
self.rs_cfg_unit_l = TextBox{parent=rs_c_4,x=25,y=7,width=7,text="Unit ID"}
self.rs_cfg_unit = NumberField{parent=rs_c_4,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local function set_bundled(bundled)
if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
end
self.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
self.rs_cfg_shortcut = TextBox{parent=rs_c_4,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
self.rs_cfg_shortcut.hide(true)
self.rs_cfg_bundled = Checkbox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_bundled = Checkbox{parent=rs_c_4,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color = Radio2D{parent=rs_c_4,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color.disable()
local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local rs_err = TextBox{parent=rs_c_4,x=8,y=14,width=30,text="Unit ID invalid.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
rs_err.hide(true)
local function back_from_rs_opts()
rs_err.hide(true)
if self.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end
if self.rs_cfg_editing ~= false then rs_pane.set_value(2) else rs_pane.set_value(3) end
end
local function save_rs_entry()
assert(self.rs_cfg_phy ~= false, "tried to save a redstone entry without a phy")
local port = self.rs_cfg_port
local u = tonumber(self.rs_cfg_unit.get_value())
@@ -313,12 +413,23 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local def = {
unit = tri(PORT_DSGN[port] == 1, u, nil),
port = port,
relay = self.rs_cfg_phy,
side = side_options_map[side.get_value()],
color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil),
invert = self.rs_cfg_inverted.get_value() or nil
}
if self.rs_cfg_editing == false then
-- check for duplicate inputs for this unit/facility
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then
rs_pane.set_value(7)
return
end
end
end
table.insert(tmp_cfg.Redstone, def)
else
def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port
@@ -331,13 +442,14 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
table.insert(tmp_cfg.Redstone, {
unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
port = IO.WASTE_PU + i,
relay = self.rs_cfg_phy,
side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil)
})
end
end
rs_pane.set_value(1)
rs_pane.set_value(2)
tool_ctl.gen_rs_summary()
side.set_value(1)
@@ -349,30 +461,35 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
else rs_err.show() end
end
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_advanced = PushButton{parent=rs_c_3,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(8)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_advanced = PushButton{parent=rs_c_4,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(9)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
TextBox{parent=rs_c_5,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_5,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."}
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
TextBox{parent=rs_c_7,x=1,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."}
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_8,x=1,y=1,height=5,text="Advanced Options"}
self.rs_cfg_inverted = Checkbox{parent=rs_c_8,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg}
TextBox{parent=rs_c_8,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_8,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
TextBox{parent=rs_c_8,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_8,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_9,x=1,y=1,height=5,text="Advanced Options"}
self.rs_cfg_inverted = Checkbox{parent=rs_c_9,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg}
TextBox{parent=rs_c_9,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
PushButton{parent=rs_c_9,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_10,x=1,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
PushButton{parent=rs_c_10,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -422,7 +539,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
side.set_value(side_to_idx(def.side))
self.rs_cfg_color.set_value(value)
self.rs_cfg_inverted.set_value(def.invert or false)
rs_pane.set_value(3)
rs_pane.set_value(4)
end
local function delete_rs_entry(idx)
@@ -432,34 +549,41 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
-- generate the redstone summary list
function tool_ctl.gen_rs_summary()
assert(self.rs_cfg_phy ~= false, "tried to generate a summary without a phy set")
rs_list.remove_all()
local modified = #ini_cfg.Redstone ~= #tmp_cfg.Redstone
local ini = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy)
local tmp = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local modified = #ini ~= #tmp
for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i]
local name = rsio.to_string(def.port)
local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple)
local conn = def.side
local unit = util.strval(def.unit or "F")
if def.relay == self.rs_cfg_phy then
local name = rsio.to_string(def.port)
local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple)
local conn = def.side
local unit = util.strval(def.unit or "F")
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(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}
TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),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}
local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),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}
if not modified then
local a = ini_cfg.Redstone[i]
local b = tmp_cfg.Redstone[i]
if not modified then
local a = ini_cfg.Redstone[i]
local b = tmp_cfg.Redstone[i]
modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert)
modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.relay ~= b.relay) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert)
end
end
end

View File

@@ -36,7 +36,8 @@ local changes = {
{ "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.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } },
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } }
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } },
{ "v1.12.0", { "Added support for redstone relays" } }
}
---@class rtu_configurator
@@ -76,6 +77,7 @@ local tool_ctl = {
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
update_peri_list = nil, ---@type function
update_relay_list = nil, ---@type function
gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function
}
@@ -130,7 +132,7 @@ end
---@param data rtu_rs_definition[]
function tool_ctl.deep_copy_rs(data)
local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color, invert = d.invert }) end
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, relay = d.relay, side = d.side, color = d.color, invert = d.invert }) end
return array
end
@@ -210,7 +212,6 @@ local function config_view(display)
end
local function show_rs_conns()
tool_ctl.gen_rs_summary()
main_pane.set_value(9)
end
@@ -350,10 +351,12 @@ function configurator.configure(ask_config)
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
end
if event == "terminate" then return end

View File

@@ -11,10 +11,14 @@ local digital_write = rsio.digital_write
-- create new redstone device
---@nodiscard
---@param relay? table optional redstone relay to use instead of the computer's redstone interface
---@return rtu_rs_device interface, boolean faulted
function redstone_rtu.new()
function redstone_rtu.new(relay)
local unit = rtu.init_unit()
-- physical interface to use
local phy = relay or rs
-- get RTU interface
local interface = unit.interface()
@@ -30,104 +34,114 @@ function redstone_rtu.new()
write_holding_reg = interface.write_holding_reg
}
-- change the phy in use (a relay or rs)
---@param new_phy table
function public.remount_phy(new_phy) phy = new_phy end
-- NOTE: for runtime speed, inversion logic results in extra code here but less code when functions are called
-- link digital input
---@param side string
---@param color integer
---@param invert boolean|nil
---@return integer count count of digital inputs
function public.link_di(side, color, invert)
local f_read ---@type function
local f_read ---@type function
if color then
if invert then
f_read = function () return digital_read(not rs.testBundledInput(side, color)) end
f_read = function () return digital_read(not phy.testBundledInput(side, color)) end
else
f_read = function () return digital_read(rs.testBundledInput(side, color)) end
f_read = function () return digital_read(phy.testBundledInput(side, color)) end
end
else
if invert then
f_read = function () return digital_read(not rs.getInput(side)) end
f_read = function () return digital_read(not phy.getInput(side)) end
else
f_read = function () return digital_read(rs.getInput(side)) end
f_read = function () return digital_read(phy.getInput(side)) end
end
end
unit.connect_di(f_read)
return unit.connect_di(f_read)
end
-- link digital output
---@param side string
---@param color integer
---@param invert boolean|nil
---@return integer count count of digital outputs
function public.link_do(side, color, invert)
local f_read ---@type function
local f_write ---@type function
local f_read ---@type function
local f_write ---@type function
if color then
if invert then
f_read = function () return digital_read(not colors.test(rs.getBundledOutput(side), color)) end
f_read = function () return digital_read(not colors.test(phy.getBundledOutput(side), color)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = rs.getBundledOutput(side)
local output = phy.getBundledOutput(side)
-- inverted conditions
if digital_write(level) then
output = colors.subtract(output, color)
else output = colors.combine(output, color) end
rs.setBundledOutput(side, output)
phy.setBundledOutput(side, output)
end
end
else
f_read = function () return digital_read(colors.test(rs.getBundledOutput(side), color)) end
f_read = function () return digital_read(colors.test(phy.getBundledOutput(side), color)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = rs.getBundledOutput(side)
local output = phy.getBundledOutput(side)
if digital_write(level) then
output = colors.combine(output, color)
else output = colors.subtract(output, color) end
rs.setBundledOutput(side, output)
phy.setBundledOutput(side, output)
end
end
end
else
if invert then
f_read = function () return digital_read(not rs.getOutput(side)) end
f_read = function () return digital_read(not phy.getOutput(side)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
rs.setOutput(side, not digital_write(level))
phy.setOutput(side, not digital_write(level))
end
end
else
f_read = function () return digital_read(rs.getOutput(side)) end
f_read = function () return digital_read(phy.getOutput(side)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
rs.setOutput(side, digital_write(level))
phy.setOutput(side, digital_write(level))
end
end
end
end
unit.connect_coil(f_read, f_write)
return unit.connect_coil(f_read, f_write)
end
-- link analog input
---@param side string
---@return integer count count of analog inputs
function public.link_ai(side)
unit.connect_input_reg(function () return rs.getAnalogInput(side) end)
return unit.connect_input_reg(function () return phy.getAnalogInput(side) end)
end
-- link analog output
---@param side string
---@return integer count count of analog outputs
function public.link_ao(side)
unit.connect_holding_reg(
function () return rs.getAnalogOutput(side) end,
function (value) rs.setAnalogOutput(side, value) end
return unit.connect_holding_reg(
function () return phy.getAnalogOutput(side) end,
function (value) phy.setAnalogOutput(side, value) end
)
end

View File

@@ -399,43 +399,41 @@ function modbus.new(rtu_dev, use_parallel_read)
return public
end
-- create an error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@param code MODBUS_EXCODE exception code
---@return modbus_packet reply
local function excode_reply(packet, code)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
reply.make(packet.txn_id, packet.unit_id, fcode, { code })
return reply
end
-- return a SERVER_DEVICE_FAIL error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_fail(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end
-- return a SERVER_DEVICE_BUSY error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
function modbus.reply__srv_device_busy(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end
-- return a NEG_ACKNOWLEDGE error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__neg_ack(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
function modbus.reply__neg_ack(packet) return excode_reply(packet, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
function modbus.reply__gw_unavailable(packet) return excode_reply(packet, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end
return modbus

View File

@@ -19,7 +19,8 @@ local LED = require("graphics.elements.indicators.LED")
local LEDPair = require("graphics.elements.indicators.LEDPair")
local RGBLED = require("graphics.elements.indicators.RGBLED")
local LINK_STATE = types.PANEL_LINK_STATE
local LINK_STATE = types.PANEL_LINK_STATE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local ALIGN = core.ALIGN
@@ -129,30 +130,45 @@ local function init(panel, units)
-- show routine statuses
for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=util.trinary(units[i].type~=RTU_UNIT_TYPE.REDSTONE,ind_grn,cpair(style.ind_bkg,style.ind_bkg))}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end
local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3}
local relay_counter = 0
-- show hardware statuses
for i = 1, list_length do
local unit = units[i]
local is_rs = unit.type == RTU_UNIT_TYPE.REDSTONE
-- hardware status
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + 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=get_name(unit.type),width=15}
local function get_name()
if is_rs then
local is_local = unit.name == "redstone_local"
relay_counter = relay_counter + util.trinary(is_local, 0, 1)
return util.c("REDSTONE", util.trinary(is_local, "", " RELAY " .. relay_counter))
else
return util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", util.trinary(util.is_int(unit.index), unit.index, ""))
end
end
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(),width=util.trinary(is_rs,24,15)}
name_box.register(databus.ps, "unit_type_" .. i, function () name_box.set_value(get_name()) end)
-- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
if unit.reactor then
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
end
end
end

View File

@@ -342,13 +342,7 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[i]
if unit.type ~= nil then
local advert = { unit.type, unit.index, unit.reactor }
if unit.type == RTU_UNIT_TYPE.REDSTONE then
insert(advert, unit.device)
end
insert(advertisement, advert)
insert(advertisement, { unit.type, unit.index, unit.reactor or -1, unit.rs_conns })
end
end
@@ -481,9 +475,10 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[packet.unit_id]
local unit_dbg_tag = " (unit " .. packet.unit_id .. ")"
if unit.name == "redstone_io" then
if unit.type == RTU_UNIT_TYPE.REDSTONE then
-- immediately execute redstone RTU requests
return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end
@@ -500,7 +495,7 @@ function rtu.comms(version, nic, conn_watchdog)
unit.pkt_queue.push_packet(packet)
end
else
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag)
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end
end
else

View File

@@ -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.12.0"
local RTU_VERSION = "v1.12.2"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
@@ -147,32 +147,36 @@ local function main()
local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals
-- get a string representation of a port interface
---@param entry rtu_rs_definition
---@return string
local function entry_iface_name(entry)
return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
end
-- configure RTU gateway based on settings file definitions
local function sys_config()
-- redstone interfaces
local rs_rtus = {} ---@type { rtu: rtu_rs_device, capabilities: IO_PORT[] }[]
--#region Redstone Interfaces
local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[]
local all_conns = { [0] = {}, {}, {}, {}, {} }
-- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do
local entry = rtu_redstone[entry_idx]
local assignment
local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
local phy = entry.relay or 0
local phy_name = entry.relay or "local"
local iface_name = entry_iface_name(entry)
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
---@cast for_reactor integer
assignment = "reactor unit " .. entry.unit
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> 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("sys_config> allocated redstone RTU for the facility"))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
else
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message)
@@ -180,14 +184,44 @@ local function main()
return false
end
-- create the appropriate RTU if it doesn't exist and check relay name validity
if entry.relay then
if type(entry.relay) ~= "string" then
local message = util.c("sys_config> invalid redstone relay '", entry.relay, '"')
println(message)
log.fatal(message)
return false
elseif not rs_rtus[entry.relay] then
log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay))
local hw_state = RTU_HW_STATE.OK
local relay = ppm.get_periph(entry.relay)
if not relay then
hw_state = RTU_HW_STATE.OFFLINE
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected"))
local _, v_device = ppm.mount_virtual()
relay = v_device
elseif ppm.get_type(entry.relay) ~= "redstone_relay" then
hw_state = RTU_HW_STATE.FAULTED
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay"))
end
rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } }
end
elseif rs_rtus[0] == nil then
log.debug(util.c("sys_config> allocated local redstone RTU"))
rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } }
end
-- verify configuration
local valid = false
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
end
local rs_rtu = rs_rtus[for_reactor].rtu
local capabilities = rs_rtus[for_reactor].capabilities
local bank = rs_rtus[phy].banks[for_reactor]
local conns = all_conns[for_reactor]
if not valid then
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
@@ -199,73 +233,105 @@ local function main()
local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
rs_rtu.link_di(entry.side, entry.color, entry.invert)
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(entry.side, entry.color, entry.invert)
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
rs_rtu.link_ai(entry.side)
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.ANALOG_OUT then
rs_rtu.link_ao(entry.side)
elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then
table.insert(bank, entry)
else
-- should be unreachable code, we already validated ports
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true)
log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx)
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(capabilities, entry.port)
table.insert(conns, entry.port)
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
end
end
-- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do
---@class rtu_registry_entry
for _, def in pairs(rs_rtus) do
local rtu_conns = { [0] = {}, {}, {}, {}, {} }
-- connect the IO banks
for for_reactor = 0, #def.banks do
local bank = def.banks[for_reactor]
local conns = rtu_conns[for_reactor]
local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility")
-- link redstone to the RTU
for i = 1, #bank do
local conn = bank[i]
local phy_name = conn.relay or "local"
local mode = rsio.get_io_mode(conn.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
def.rtu.link_di(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
def.rtu.link_do(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.ANALOG_IN then
def.rtu.link_ai(conn.side)
elseif mode == rsio.IO_MODE.ANALOG_OUT then
def.rtu.link_ao(conn.side)
else
log.fatal(util.c("sys_config> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(conns, conn.port)
log.debug(util.c("sys_config> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign))
end
end
---@type rtu_registry_entry
local unit = {
uid = 0, ---@type integer
name = "redstone_io", ---@type string
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = false, ---@type integer|false
reactor = for_reactor, ---@type integer
device = def.capabilities, ---@type IO_PORT[] use device field for redstone ports
is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil
hw_state = RTU_HW_STATE.OK, ---@type RTU_HW_STATE
rtu = def.rtu, ---@type rtu_device|rtu_rs_device
uid = 0,
name = def.name,
type = RTU_UNIT_TYPE.REDSTONE,
index = false,
reactor = nil,
device = def.phy,
rs_conns = rtu_conns,
is_multiblock = false,
formed = nil,
hw_state = def.hw_state,
rtu = def.rtu,
modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
pkt_queue = nil,
thread = nil
}
table.insert(units, unit)
local for_message = "facility"
if util.is_int(for_reactor) then
for_message = util.c("reactor unit ", for_reactor)
end
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
-- mounted peripherals
--#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name
@@ -446,19 +512,20 @@ local function main()
---@class rtu_registry_entry
local rtu_unit = {
uid = 0, ---@type integer
name = name, ---@type string
type = rtu_type, ---@type RTU_UNIT_TYPE
index = index or false, ---@type integer|false
reactor = for_reactor, ---@type integer
device = device, ---@type table peripheral reference
is_multiblock = is_multiblock, ---@type boolean
formed = formed, ---@type boolean|nil
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil
thread = nil ---@type parallel_thread|nil
uid = 0, ---@type integer RTU unit ID
name = name, ---@type string unit name
type = rtu_type, ---@type RTU_UNIT_TYPE unit type
index = index or false, ---@type integer|false device index
reactor = for_reactor, ---@type integer|nil unit/facility assignment
device = device, ---@type table peripheral reference
rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections
is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral
formed = formed, ---@type boolean|nil if this peripheral is currently formed
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface
modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface
pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue
thread = nil ---@type parallel_thread|nil associated RTU thread
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
@@ -492,6 +559,8 @@ local function main()
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end
--#endregion
return true
end
@@ -502,17 +571,6 @@ local function main()
log.debug("boot> running sys_config()")
if sys_config() then
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- check comms modem
if smem_dev.modem == nil then
println("startup> comms modem not found")
@@ -534,6 +592,17 @@ local function main()
databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
@@ -560,7 +629,7 @@ local function main()
-- run threads
parallel.waitForAll(table.unpack(_threads))
else
println("configuration failed, exiting...")
println("system initialization failed, exiting...")
end
renderer.close_ui()

View File

@@ -132,6 +132,8 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.REDSTONE then
unit.rtu.remount_phy(device)
else
unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)