#580 RTU gateway multi-modem wired/wireless failover networking

This commit is contained in:
Mikayla Fischler 2025-10-18 17:23:06 -04:00
parent fe9ee313f9
commit 1fcc91e98b
6 changed files with 162 additions and 42 deletions

View File

@ -134,7 +134,7 @@ local function main()
-- get the configured modem
if smem_dev.modem_wired then
smem_dev.modem = ppm.get_wired_modem(smem_dev.modem_iface)
smem_dev.modem = ppm.get_modem(smem_dev.modem_iface)
else smem_dev.modem = ppm.get_wireless_modem() end
-- initial state evaluation

View File

@ -41,7 +41,7 @@ function backplane.init(config, __shared_memory)
-- init wired NIC
if _bp.lan_en then
local modem = ppm.get_wired_modem(_bp.lan_iface)
local modem = ppm.get_modem(_bp.lan_iface)
if modem then
_bp.wd_nic = network.nic(modem)
@ -62,7 +62,7 @@ function backplane.init(config, __shared_memory)
-- grab the preferred active NIC
if _bp.wlan_pref then
_bp.wl_act = true
_bp.act_nic = _bp.wl_nics[1]
_bp.act_nic = _bp.wl_nic
else
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
@ -110,11 +110,11 @@ function backplane.detach(type, device, iface)
local was_wl = wl_nic and wl_nic.is_modem(device)
if wd_nic and was_wd then
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
elseif wl_nic and was_wl then
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
end
-- we only care if this is our active comms modem

View File

@ -90,22 +90,73 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=1,text="Please select the network interface(s)."}
TextBox{parent=net_c_1,x=41,y=1,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_1,x=1,y=8,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"}
local rtu_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
local wl_pref = Checkbox{parent=net_c_1,x=30,y=3,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg,callback=function()end}
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function dis_pref(value)
if not value then
wl_pref.set_value(false)
wl_pref.disable()
else wl_pref.enable() end
end
dis_pref(ini_cfg.WirelessModem)
local function on_wired_change(value)
tool_ctl.gen_modem_list(value)
end
local wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=dis_pref}
local wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting to peripherals will cause problems",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.get_value()
tmp_cfg.PreferWireless = wl_pref.get_value()
if not wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (wired.get_value() or wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
net_pane.set_value(2)
modem_err.hide(true)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_interfaces,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_2,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=11,text="RTU Channel"}
local rtu_chan = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c = tonumber(svr_chan.get_value())
@ -113,7 +164,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
if svr_c ~= nil and rtu_c ~= nil then
tmp_cfg.SVR_Channel = svr_c
tmp_cfg.RTU_Channel = rtu_c
net_pane.set_value(2)
net_pane.set_value(3)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
@ -124,19 +175,19 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Connection Timeout"}
local timeout = NumberField{parent=net_c_3,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,text="Trusted Range"}
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=8,text="Trusted Range (Wireless Only)"}
local range = NumberField{parent=net_c_3,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
local p2_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local p2_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value())
@ -144,7 +195,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = range_val
net_pane.set_value(3)
net_pane.set_value(4)
p2_err.hide(true)
elseif timeout_val == nil then
p2_err.set_value("Please set the connection timeout.")
@ -155,23 +206,23 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for wireless security on multiplayer servers. All devices on the same wireless network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Auth Key (Wireless Only, Not Used when Wired)"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
@ -182,8 +233,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else key_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@ -382,6 +433,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
load_settings(ini_cfg)
try_set(s_vol, ini_cfg.SpeakerVolume)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(wl_pref, ini_cfg.PreferWireless)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(rtu_chan, ini_cfg.RTU_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
@ -665,6 +719,60 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list(enable)
modem_list.remove_all()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list(true)
end
local modems = ppm.get_wired_modem_list()
local missing = { tmp = true, ini = true }
for iface, _ in pairs(modems) do
if ini_cfg.WiredModem == iface then
missing.ini = false
elseif tmp_cfg.WiredModem == iface then
missing.tmp = false
end
end
if missing.tmp and tmp_cfg.WiredModem then
local line = Div{parent=modem_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=4,text="Used",fg_bg=cpair(tri(enable,colors.blue,colors.gray),colors.white)}
PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}.disable()
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=tmp_cfg.WiredModem}
end
if missing.ini and ini_cfg.WiredModem and (tmp_cfg.WiredModem ~= ini_cfg.WiredModem) then
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == ini_cfg.WiredModem
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(ini_cfg.WiredModem)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text="[missing]",fg_bg=cpair(colors.red,colors.white)}
TextBox{parent=line,x=25,y=1,text=ini_cfg.WiredModem}
if used or not enable then select_btn.disable() end
end
-- list wired modems
for iface, _ in pairs(modems) do
local line = Div{parent=modem_list,x=1,y=1,height=1}
local used = tmp_cfg.WiredModem == iface
TextBox{parent=line,x=1,y=1,width=4,text=tri(used,"Used","----"),fg_bg=cpair(tri(used and enable,colors.blue,colors.gray),colors.white)}
local select_btn = PushButton{parent=line,x=6,y=1,min_width=8,height=1,text="SELECT",callback=function()select(iface)end,fg_bg=cpair(colors.black,colors.lightBlue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=g_lg_fg_bg}
TextBox{parent=line,x=15,y=1,text=iface}
if used or not enable then select_btn.disable() end
end
end
--#endregion
end

View File

@ -37,7 +37,8 @@ local changes = {
{ "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.12.0", { "Added support for redstone relays" } }
{ "v1.12.0", { "Added support for redstone relays" } },
{ "v1.13.0", { "Added support for wired communications modems" } }
}
---@class rtu_configurator
@ -80,6 +81,8 @@ local tool_ctl = {
update_relay_list = nil, ---@type function
gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function
gen_modem_list = function (_) end
}
---@class rtu_config
@ -112,7 +115,9 @@ local fields = {
{ "SVR_Channel", "SVR Channel", 16240 },
{ "RTU_Channel", "RTU Channel", 16242 },
{ "ConnTimeout", "Connection Timeout", 5 },
{ "WirelessModem", "Wireless Modem", true },
{ "WiredModem", "Wired Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "TrustedRange", "Trusted Range", 0 },
{ "AuthKey", "Facility Auth Key", "" },
{ "LogMode", "Log Mode", log.MODE.APPEND },
@ -317,8 +322,11 @@ function configurator.configure(ask_config)
load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg)
-- set tmp_cfg so interface lists are correct
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
tmp_cfg.WiredModem = ini_cfg.WiredModem
reset_term()
@ -333,6 +341,8 @@ function configurator.configure(ask_config)
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_modem_list(ini_cfg.WiredModem ~= false)
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@ -354,11 +364,13 @@ function configurator.configure(ask_config)
ppm.handle_unmount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
tool_ctl.gen_modem_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
tool_ctl.gen_modem_list()
end
if event == "terminate" then return end

View File

@ -447,15 +447,15 @@ end
---@return table|nil reactor function table
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
-- get a wired modem by name
-- get a modem by name
---@nodiscard
---@param iface string CC peripheral interface
---@return Modem|nil modem function table
function ppm.get_wired_modem(iface)
function ppm.get_modem(iface)
local modem = nil
local device = ppm_sys.mounts[iface]
if device.type == "modem" then modem = device.dev end
if device and device.type == "modem" then modem = device.dev end
return modem
end

View File

@ -68,7 +68,7 @@ function pcie_bus.init(config, println)
if type(config.WiredModem) == "string" then
bus.wired_modem = config.WiredModem
local wired_modem = ppm.get_wired_modem(bus.wired_modem)
local wired_modem = ppm.get_modem(bus.wired_modem)
if not (wired_modem and bus.wired_modem) then
println("startup> wired comms modem not found")