Merge pull request #646 from MikaylaFischler/580-wired-comms-networking

580 wired comms networking
This commit is contained in:
Mikayla
2025-11-09 16:06:33 -05:00
committed by GitHub
59 changed files with 4004 additions and 2009 deletions

494
coordinator/backplane.lua Normal file
View File

@@ -0,0 +1,494 @@
--
-- Coordinator System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local sounder = require("coordinator.sounder")
local println = util.println
local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms
---@class crd_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type crd_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic
wd_nic = nil, ---@type nic|nil
wl_nic = nil, ---@type nic|nil
speaker = nil, ---@type Speaker|nil
---@class crd_displays
displays = {
main = nil, ---@type Monitor|nil
main_iface = "",
flow = nil, ---@type Monitor|nil
flow_iface = "",
unit_displays = {}, ---@type Monitor[]
unit_ifaces = {} ---@type string[]
}
}
-- initialize the display peripheral backplane
---@param config crd_config
---@return boolean success, string error_msg
function backplane.init_displays(config)
local displays = _bp.displays
local w, h, _
log.info("BKPLN: DISPLAY INIT")
-- monitor configuration verification
local mon_cfv = util.new_validator()
mon_cfv.assert_type_str(config.MainDisplay)
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
for i = 1, #config.UnitDisplays do
mon_cfv.assert_type_str(config.UnitDisplays[i])
end
if not mon_cfv.valid() then
return false, "Monitor configuration invalid."
end
-- setup and check display peripherals
-- main display
local disp, iface = ppm.get_periph(config.MainDisplay), config.MainDisplay
displays.main = disp
displays.main_iface = iface
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " MAIN/" .. iface)
iocontrol.fp_monitor_state("main", util.trinary(disp, 2, 1))
if not disp then
return false, "Main monitor is not connected."
end
disp.setTextScale(0.5)
w, _ = ppm.monitor_block_size(disp.getSize())
if w ~= 8 then
log.info("BKPLN: DISPLAY MAIN/" .. iface .. " BAD RESOLUTION")
return false, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
end
-- flow display
if not config.DisableFlowView then
disp, iface = ppm.get_periph(config.FlowDisplay), config.FlowDisplay
displays.flow = disp
displays.flow_iface = iface
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " FLOW/" .. iface)
iocontrol.fp_monitor_state("flow", util.trinary(disp, 2, 1))
if not disp then
return false, "Flow monitor is not connected."
end
disp.setTextScale(0.5)
w, _ = ppm.monitor_block_size(disp.getSize())
if w ~= 8 then
log.info("BKPLN: DISPLAY FLOW/" .. iface .. " BAD RESOLUTION")
return false, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end
-- unit display(s)
for i = 1, config.UnitCount do
disp, iface = ppm.get_periph(config.UnitDisplays[i]), config.UnitDisplays[i]
displays.unit_displays[i] = disp
displays.unit_ifaces[i] = iface
log.info("BKPLN: DISPLAY LINK_" .. util.trinary(disp, "UP", "DOWN") .. " UNIT_" .. i .. "/" .. iface)
iocontrol.fp_monitor_state(i, util.trinary(disp, 2, 1))
if not disp then
return false, "Unit " .. i .. " monitor is not connected."
end
disp.setTextScale(0.5)
w, h = ppm.monitor_block_size(disp.getSize())
if w ~= 4 or h ~= 4 then
log.info("BKPLN: DISPLAY UNIT_" .. i .. "/" .. iface .. " BAD RESOLUTION")
return false, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
log.info("BKPLN: DISPLAY INIT OK")
return true, ""
end
-- initialize the system peripheral backplane
---@param config crd_config
---@param __shared_memory crd_shared_memory
---@return boolean success
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
-- Modem Init
-- init wired NIC
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
log_comms("wired comms modem " .. util.trinary(modem, "connected", "not found"))
_bp.wd_nic = wd_nic
_bp.act_nic = wd_nic -- set this as active for now
wd_nic.closeAll()
wd_nic.open(config.CRD_Channel)
iocontrol.fp_has_wd_modem(modem ~= nil)
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
log_comms("wireless comms modem " .. util.trinary(modem, "connected", "not found"))
-- set this as active if connected or if both modems are disconnected and this is preferred
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
_bp.act_nic = wl_nic
log.info("BKPLN: switched active to preferred wireless")
end
_bp.wl_nic = wl_nic
wl_nic.closeAll()
wl_nic.open(config.CRD_Channel)
iocontrol.fp_has_wl_modem(modem ~= nil)
end
-- at least one comms modem is required
if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then
log_comms("no comms modem found")
println("startup> no comms modem found")
log.warning("BKPLN: no comms modem on startup")
return false
end
-- Speaker Init
_bp.speaker = ppm.get_device("speaker")
if not _bp.speaker then
log_boot("annunciator alarm speaker not found")
println("startup> speaker not found")
log.fatal("BKPLN: no annunciator alarm speaker found")
return false
else
log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(_bp.speaker))
log_boot("annunciator alarm speaker connected")
local sounder_start = util.time_ms()
sounder.init(_bp.speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end
return true
end
-- get the active NIC
function backplane.active_nic() return _bp.act_nic end
-- get the wireless NIC
function backplane.wireless_nic() return _bp.wl_nic end
-- get the configured displays
function backplane.displays() return _bp.displays end
-- handle a backplane peripheral attach
---@param type string
---@param device table
---@param iface string
function backplane.attach(type, device, iface)
local MQ__RENDER_CMD = _bp.smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.crd_sys.coord_comms
if type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
log_sys("wired comms modem reconnected")
iocontrol.fp_has_wd_modem(true)
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.act_nic = wd_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
log_sys("wireless comms modem reconnected")
iocontrol.fp_has_wl_modem(true)
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.act_nic = wl_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
log_sys("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
log_sys("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
elseif type == "monitor" then
---@cast device Monitor
local is_used = false
log.info("BKPLN: DISPLAY LINK_UP " .. iface)
if _bp.displays.main_iface == iface then
is_used = true
_bp.displays.main = device
log.info("BKPLN: main display reconnected")
iocontrol.fp_monitor_state("main", 2)
elseif _bp.displays.flow_iface == iface then
is_used = true
_bp.displays.flow = device
log.info("BKPLN: flow display reconnected")
iocontrol.fp_monitor_state("flow", 2)
else
for idx, monitor in ipairs(_bp.displays.unit_ifaces) do
if monitor == iface then
is_used = true
_bp.displays.unit_displays[idx] = device
log.info("BKPLN: unit " .. idx .. " display reconnected")
iocontrol.fp_monitor_state(idx, 2)
break
end
end
end
-- notify renderer if it is using it
if is_used then
log_sys(util.c("configured monitor ", iface, " reconnected"))
_bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, iface)
else
log_sys(util.c("unused monitor ", iface, " connected"))
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_UP " .. iface)
sounder.reconnect(device)
log_sys("alarm sounder speaker reconnected")
iocontrol.fp_has_speaker(true)
end
end
-- handle a backplane peripheral detach
---@param type string
---@param device table
---@param iface string
function backplane.detach(type, device, iface)
local MQ__RENDER_CMD = _bp.smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = _bp.smem.q_types.MQ__RENDER_DATA
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.crd_sys.coord_comms
if type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
if wd_nic and wd_nic.is_modem(device) then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
iocontrol.fp_has_wd_modem(false)
elseif wl_nic and wl_nic.is_modem(device) then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
iocontrol.fp_has_wl_modem(false)
end
-- we only care if this is our active comms modem
if _bp.act_nic.is_modem(device) then
log_sys("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.act_nic == wl_nic then
-- wireless active disconnected
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if wl_nic and modem then
log_sys("found another wireless modem, using it for comms")
log.info("BKPLN: found another wireless modem, using it for comms")
wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
iocontrol.fp_has_wl_modem(true)
elseif wd_nic and wd_nic.is_connected() then
_bp.act_nic = wd_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem")
else
-- close out main UI
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status
log_sys("awaiting comms modem reconnect...")
end
elseif wl_nic and wl_nic.is_connected() then
-- wired active disconnected, wireless available
_bp.act_nic = wl_nic
_bp.smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem")
else
-- wired active disconnected, wireless unavailable
end
elseif wd_nic and wd_nic.is_modem(device) then
-- wired, but not active
log_sys("standby wired modem disconnected")
log.info("BKPLN: standby wired modem disconnected")
elseif wl_nic and wl_nic.is_modem(device) then
-- wireless, but not active
log_sys("standby wireless modem disconnected")
log.info("BKPLN: standby wireless modem disconnected")
else
log_sys("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
elseif type == "monitor" then
---@cast device Monitor
local is_used = false
log.info("BKPLN: DISPLAY LINK_DOWN " .. iface)
if _bp.displays.main == device then
is_used = true
log.info("BKPLN: main display disconnected")
iocontrol.fp_monitor_state("main", 1)
elseif _bp.displays.flow == device then
is_used = true
log.info("BKPLN: flow display disconnected")
iocontrol.fp_monitor_state("flow", 1)
else
for idx, monitor in pairs(_bp.displays.unit_displays) do
if monitor == device then
is_used = true
log.info("BKPLN: unit " .. idx .. " display disconnected")
iocontrol.fp_monitor_state(idx, 1)
break
end
end
end
-- notify renderer if it was using it
if is_used then
log_sys("lost a configured monitor")
_bp.smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, iface)
else
log_sys("lost an unused monitor")
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_DOWN " .. iface)
log_sys("alarm sounder speaker disconnected")
iocontrol.fp_has_speaker(false)
end
end
return backplane

View File

@@ -149,18 +149,39 @@ local function handle_timeout()
end
-- attempt a connection to the supervisor to get cooling info
local function sv_connect()
---@param cfg crd_config current configuration for modem settings
local function sv_connect(cfg)
self.sv_conn_button.disable()
self.sv_conn_detail.set_value("")
local modem = ppm.get_wireless_modem()
local modem = nil
if cfg.WirelessModem then
modem = ppm.get_wireless_modem()
if cfg.WiredModem then
local wd_modem = ppm.get_modem(cfg.WiredModem)
if cfg.PreferWireless then
if not modem then
modem = wd_modem
end
else
if wd_modem then
modem = wd_modem
end
end
end
elseif cfg.WiredModem then
modem = ppm.get_modem(cfg.WiredModem)
end
if modem == nil then
self.sv_conn_status.set_value("Please connect an ender/wireless modem.")
self.sv_conn_status.set_value("Could not find configured modem(s).")
else
self.sv_conn_status.set_value("Modem found, connecting...")
if self.nic == nil then self.nic = network.nic(modem) end
self.nic.closeAll()
self.nic = network.nic(modem)
self.nic.open(self.tmp_cfg.CRD_Channel)
self.sv_addr = comms.BROADCAST
@@ -209,7 +230,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect(tmp_cfg)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function sv_skip()
tcd.abort(handle_timeout)

View File

@@ -237,17 +237,16 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."}
TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=8,text="Energy Scale"}
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts()
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1

View File

@@ -2,6 +2,7 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -31,6 +32,9 @@ local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
api_en = nil, ---@type Checkbox
pkt_chan = nil, ---@type NumberField
api_timeout = nil, ---@type NumberField
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
@@ -63,100 +67,204 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
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_c_5 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_6 = 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,net_c_4}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4,net_c_5,net_c_6}}
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 3 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,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
local function on_wired_change(_) tool_ctl.gen_modem_list() end
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
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)}
TextBox{parent=net_c_1,x=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
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="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",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)}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
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 chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.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
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
self.wl_pref.enable()
else
self.wl_pref.set_value(tmp_cfg.WirelessModem)
self.wl_pref.disable()
end
if not tmp_cfg.WirelessModem then
self.api_en.set_value(false)
self.api_en.disable()
else
self.api_en.enable()
end
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(1)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,text="If you selected multiple interfaces, please specify if this device should prefer wireless or otherwise wired. The preferred interface is switched too when reconnected even if failover has succeeded onto the fallback interface."}
self.wl_pref = Checkbox{parent=net_c_2,y=7,label="Prefer Wireless",default=ini_cfg.PreferWireless,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=19,y=7,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_2,y=9,text="With a wireless modem, configure Pocket access."}
self.api_en = Checkbox{parent=net_c_2,y=11,label="Enable Pocket Access",default=ini_cfg.API_Enabled,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=24,y=11,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local function submit_net_cfg_opts()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PreferWireless = self.wl_pref.get_value()
else
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
end
tmp_cfg.API_Enabled = tri(tmp_cfg.WirelessModem, self.api_en.get_value(), false)
if tmp_cfg.API_Enabled then
self.pkt_chan.enable()
self.api_timeout.enable()
else
self.pkt_chan.disable()
self.api_timeout.disable()
end
net_pane.set_value(3)
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_net_cfg_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 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_3,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_3,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_3,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=12,width=14,text="Pocket Channel"}
self.pkt_chan = NumberField{parent=net_c_3,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(self.pkt_chan.get_value())
if not tmp_cfg.API_Enabled then pkt_c = tmp_cfg.PKT_Channel or 16244 end
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
net_pane.set_value(4)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_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=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_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_channels,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 connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_4,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_4,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,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=1,y=10,width=14,text="Pocket Timeout"}
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=10,width=14,text="Pocket Timeout"}
self.api_timeout = NumberField{parent=net_c_4,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local ct_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(self.api_timeout.get_value())
if not tmp_cfg.API_Enabled then api_cto = tmp_cfg.API_Timeout or 5 end
if svr_cto ~= nil and api_cto ~= nil then
tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto
net_pane.set_value(3)
if tmp_cfg.WirelessModem then
net_pane.set_value(5)
else
tmp_cfg.TrustedRange = 0
tmp_cfg.AuthKey = ""
network.deinit_mac()
-- prep supervisor connection screen
tool_ctl.init_sv_connect_ui()
main_pane.set_value(3)
end
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,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_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,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=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=1,text="Please set the wireless trusted range below."}
TextBox{parent=net_c_5,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local range = NumberField{parent=net_c_5,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local tr_err = TextBox{parent=net_c_5,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
comms.set_trusted_range(range_val)
net_pane.set_value(4)
net_pane.set_value(6)
tr_err.hide(true)
else tr_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_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_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 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_6,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_6,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_4,x=1,y=11,text="Facility Auth Key"}
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}
TextBox{parent=net_c_6,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
local key, _ = TextField{parent=net_c_6,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_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_6,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_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 key_err = TextBox{parent=net_c_6,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()
@@ -174,8 +282,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else key_err.show() end
end
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}
PushButton{parent=net_c_6,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_6,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -188,7 +296,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -230,10 +338,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
@@ -370,11 +478,15 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(self.api_en, ini_cfg.API_Enabled)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(self.pkt_chan, ini_cfg.PKT_Channel)
try_set(svr_timeout, ini_cfg.SVR_Timeout)
try_set(api_timeout, ini_cfg.API_Timeout)
try_set(self.api_timeout, ini_cfg.API_Timeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
@@ -574,6 +686,59 @@ 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()
modem_list.remove_all()
local enable = wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
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 end
if 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

@@ -36,7 +36,8 @@ local changes = {
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.5.1", { "Added energy scale options" } },
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } }
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } },
{ "v1.7.0", { "Added support for wired communications modems", "Added option for allowing Pocket connections" } }
}
---@class crd_configurator
@@ -89,7 +90,9 @@ local tool_ctl = {
is_int_min_max = nil, ---@type function
update_mon_reqs = nil, ---@type function
gen_mon_list = function () end
gen_mon_list = function () end,
gen_modem_list = function () end
}
---@class crd_config
@@ -104,6 +107,10 @@ local tmp_cfg = {
MainDisplay = nil, ---@type string
FlowDisplay = nil, ---@type string
UnitDisplays = {}, ---@type string[]
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
API_Enabled = true,
SVR_Channel = nil, ---@type integer
CRD_Channel = nil, ---@type integer
PKT_Channel = nil, ---@type integer
@@ -136,6 +143,10 @@ local fields = {
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "API_Enabled", "Pocket API Connectivity", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "CRD_Channel", "CRD Channel", 16243 },
{ "PKT_Channel", "PKT Channel", 16244 },
@@ -327,6 +338,9 @@ function configurator.configure(start_code, message)
-- copy in some important values to start with
preset_monitor_fields()
-- this needs to be initialized as it is used before being set
tmp_cfg.WiredModem = ini_cfg.WiredModem
reset_term()
ppm.mount_all()
@@ -341,6 +355,7 @@ function configurator.configure(start_code, message)
config_view(display)
tool_ctl.gen_mon_list()
tool_ctl.gen_modem_list()
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -364,8 +379,10 @@ function configurator.configure(start_code, message)
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_mon_list()
tool_ctl.gen_modem_list()
elseif event == "monitor_resize" then
tool_ctl.gen_mon_list()
tool_ctl.gen_modem_list()
elseif event == "modem_message" then
facility.receive_sv(param1, param2, param3, param4, param5)
end

View File

@@ -1,6 +1,5 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local types = require("scada-common.types")
@@ -29,11 +28,9 @@ local config = {}
coordinator.config = config
-- load the coordinator configuration<br>
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
-- load the coordinator configuration
function coordinator.load_config()
if not settings.load("/coordinator.settings") then return 1 end
if not settings.load("/coordinator.settings") then return false end
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
@@ -47,6 +44,10 @@ function coordinator.load_config()
config.FlowDisplay = settings.get("FlowDisplay")
config.UnitDisplays = settings.get("UnitDisplays")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.API_Enabled = settings.get("API_Enabled")
config.SVR_Channel = settings.get("SVR_Channel")
config.CRD_Channel = settings.get("CRD_Channel")
config.PKT_Channel = settings.get("PKT_Channel")
@@ -80,6 +81,13 @@ function coordinator.load_config()
cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_type_bool(config.WirelessModem)
cfv.assert((config.WiredModem == false) or (type(config.WiredModem) == "string"))
cfv.assert(config.WirelessModem or (type(config.WiredModem) == "string"))
cfv.assert_type_bool(config.PreferWireless)
cfv.assert_type_bool(config.API_Enabled)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.CRD_Channel)
cfv.assert_channel(config.PKT_Channel)
@@ -110,85 +118,7 @@ function coordinator.load_config()
cfv.assert_type_int(config.ColorMode)
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
-- Monitor Setup
---@class monitors_struct
local monitors = {
main = nil, ---@type Monitor|nil
main_name = "",
flow = nil, ---@type Monitor|nil
flow_name = "",
unit_displays = {}, ---@type Monitor[]
unit_name_map = {} ---@type string[]
}
local mon_cfv = util.new_validator()
-- get all interface names
local names = {}
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
local function setup_monitors()
mon_cfv.assert_type_str(config.MainDisplay)
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
if mon_cfv.valid() then
local w, h, _
if not util.table_contains(names, config.MainDisplay) then
return 2, "Main monitor is not connected."
end
monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
monitors.main.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.main.getSize())
if w ~= 8 then
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
end
if not config.DisableFlowView then
if not util.table_contains(names, config.FlowDisplay) then
return 2, "Flow monitor is not connected."
end
monitors.flow = ppm.get_periph(config.FlowDisplay)
monitors.flow_name = config.FlowDisplay
monitors.flow.setTextScale(0.5)
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
if w ~= 8 then
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
end
end
for i = 1, config.UnitCount do
local display = config.UnitDisplays[i]
if type(display) ~= "string" or not util.table_contains(names, display) then
return 2, "Unit " .. i .. " monitor is not connected."
end
monitors.unit_displays[i] = ppm.get_periph(display)
monitors.unit_name_map[i] = display
monitors.unit_displays[i].setTextScale(0.5)
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
if w ~= 4 or h ~= 4 then
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
end
end
else return 2, "Monitor configuration invalid." end
end
if cfv.valid() then
local ok, result, message = pcall(setup_monitors)
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
if result == 2 then return 2, message end
else return 1 end
return 0, monitors
return cfv.valid()
end
-- dmesg print wrapper
@@ -232,9 +162,10 @@ end
-- coordinator communications
---@nodiscard
---@param version string coordinator version
---@param nic nic network interface device
---@param nic nic active network interface device
---@param wl_nic nic|nil pocket wireless network interface device
---@param sv_watchdog watchdog
function coordinator.comms(version, nic, sv_watchdog)
function coordinator.comms(version, nic, wl_nic, sv_watchdog)
local self = {
sv_linked = false,
sv_addr = comms.BROADCAST,
@@ -249,16 +180,16 @@ function coordinator.comms(version, nic, sv_watchdog)
est_task_done = nil
}
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
-- configure network channels
nic.closeAll()
nic.open(config.CRD_Channel)
end
-- pass config to apisessions
apisessions.init(nic, config)
if config.API_Enabled and wl_nic then
apisessions.init(wl_nic, config)
end
-- PRIVATE FUNCTIONS --
--#region PRIVATE FUNCTIONS --
-- send a packet to the supervisor
---@param msg_type MGMT_TYPE|CRDN_TYPE
@@ -293,7 +224,8 @@ function coordinator.comms(version, nic, sv_watchdog)
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
---@diagnostic disable-next-line: need-check-nil
wl_nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
self.last_api_est_acks[packet.src_addr()] = ack
end
@@ -309,11 +241,20 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
-- PUBLIC FUNCTIONS --
--#endregion
--#region PUBLIC FUNCTIONS --
---@class coord_comms
local public = {}
-- switch the current active NIC
---@param act_nic nic
function public.switch_nic(act_nic)
public.close()
nic = act_nic
end
-- try to connect to the supervisor if not already linked
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
---@return boolean ok, boolean start_ui
@@ -468,7 +409,9 @@ function coordinator.comms(version, nic, sv_watchdog)
if l_chan ~= config.CRD_Channel then
log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == config.PKT_Channel then
if not self.sv_linked then
if not config.API_Enabled then
-- log.debug("discarding pocket API packet due to the API being disabled")
elseif not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
@@ -784,6 +727,8 @@ function coordinator.comms(version, nic, sv_watchdog)
---@nodiscard
function public.is_linked() return self.sv_linked end
--#endregion
return public
end

View File

@@ -34,18 +34,10 @@ local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
local iocontrol = {}
---@class ioctl
local io = {}
-- initialize front panel PSIL
---@param firmware_v string coordinator version
---@param comms_v string comms version
function iocontrol.init_fp(firmware_v, comms_v)
local io = {
---@class ioctl_front_panel
io.fp = { ps = psil.create() }
io.fp.ps.publish("version", firmware_v)
io.fp.ps.publish("comms_version", comms_v)
end
fp = { ps = psil.create() }
}
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
@@ -290,9 +282,21 @@ end
-- toggle heartbeat indicator
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
-- report presence of the wireless modem
-- report versions to front panel
---@param firmware_v string coordinator version
---@param comms_v string comms version
function iocontrol.fp_versions(firmware_v, comms_v)
io.fp.ps.publish("version", firmware_v)
io.fp.ps.publish("comms_version", comms_v)
end
-- report presence of the wired comms modem
---@param has_modem boolean
function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end
function iocontrol.fp_has_wd_modem(has_modem) io.fp.ps.publish("has_wd_modem", has_modem) end
-- report presence of the wireless comms modem
---@param has_modem boolean
function iocontrol.fp_has_wl_modem(has_modem) io.fp.ps.publish("has_wl_modem", has_modem) end
-- report presence of the speaker
---@param has_speaker boolean
@@ -304,6 +308,7 @@ function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) en
-- report monitor connection state
---@param id string|integer unit ID for unit monitor, "main" for main monitor, or "flow" for flow monitor
---@param connected 1|2|3 1 for disconnected, 2 for connected but no view (may not fit), 3 for connected with view rendered
function iocontrol.fp_monitor_state(id, connected)
local name = nil

View File

@@ -28,8 +28,9 @@ local renderer = {}
-- render engine
local engine = {
config = nil, ---@type crd_config
color_mode = 1, ---@type COLOR_MODE
monitors = nil, ---@type monitors_struct|nil
monitors = nil, ---@type crd_displays|nil
dmesg_window = nil, ---@type Window|nil
ui_ready = false,
fp_ready = false,
@@ -76,25 +77,18 @@ end
-- apply renderer configurations
---@param config crd_config
function renderer.configure(config)
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
engine.config = config
engine.color_mode = config.ColorMode
engine.disable_flow_view = config.DisableFlowView
end
-- link to the monitor peripherals
---@param monitors monitors_struct
function renderer.set_displays(monitors)
engine.monitors = monitors
-- report to front panel as connected
iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil)
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
style.set_themes(config.MainTheme, config.FrontPanelTheme, config.ColorMode)
end
-- init all displays in use by the renderer
function renderer.init_displays()
---@param monitors crd_displays
function renderer.init_displays(monitors)
engine.monitors = monitors
-- init main and flow monitors
_init_display(engine.monitors.main)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
@@ -138,7 +132,7 @@ function renderer.try_start_fp()
-- show front panel view on terminal
status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
panel_view(engine.ui.front_panel, engine.config)
end)
if status then
@@ -199,6 +193,7 @@ function renderer.try_start_ui()
if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display)
iocontrol.fp_monitor_state("main", 3)
util.nop()
end
@@ -206,6 +201,7 @@ function renderer.try_start_ui()
if engine.monitors.flow ~= nil then
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
flow_view(engine.ui.flow_display)
iocontrol.fp_monitor_state("flow", 3)
util.nop()
end
@@ -213,6 +209,7 @@ function renderer.try_start_ui()
for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[idx], idx)
iocontrol.fp_monitor_state(idx, 3)
util.nop()
end
end)
@@ -239,9 +236,21 @@ function renderer.close_ui()
end
-- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
if engine.ui.main_display ~= nil then
engine.ui.main_display.delete()
iocontrol.fp_monitor_state("main", 2)
end
if engine.ui.flow_display ~= nil then
engine.ui.flow_display.delete()
iocontrol.fp_monitor_state("flow", 2)
end
for idx, display in pairs(engine.ui.unit_displays) do
display.delete()
iocontrol.fp_monitor_state(idx, 2)
end
-- report ui as not ready
engine.ui_ready = false
@@ -275,90 +284,51 @@ function renderer.fp_ready() return engine.fp_ready end
function renderer.ui_ready() return engine.ui_ready end
-- handle a monitor peripheral being disconnected
---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device)
local is_used = false
---@param iface string monitor interface
function renderer.handle_disconnect(iface)
if not engine.monitors then return false end
if engine.monitors.main == device then
if engine.monitors.main_iface == iface then
if engine.ui.main_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.main_display.delete()
log_render("closed main view due to monitor disconnect")
end
is_used = true
engine.monitors.main = nil
engine.ui.main_display = nil
iocontrol.fp_monitor_state("main", false)
elseif engine.monitors.flow == device then
elseif engine.monitors.flow_iface == iface then
if engine.ui.flow_display ~= nil then
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
log_render("closed flow view due to monitor disconnect")
end
is_used = true
engine.monitors.flow = nil
engine.ui.flow_display = nil
iocontrol.fp_monitor_state("flow", false)
else
for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
for idx, u_iface in pairs(engine.monitors.unit_ifaces) do
if u_iface == iface then
if engine.ui.unit_displays[idx] ~= nil then
-- delete element tree and clear root UI elements
engine.ui.unit_displays[idx].delete()
log_render("closed unit" .. idx .. "view due to monitor disconnect")
end
is_used = true
engine.monitors.unit_displays[idx] = nil
engine.ui.unit_displays[idx] = nil
iocontrol.fp_monitor_state(idx, false)
break
end
end
end
return is_used
end
-- handle a monitor peripheral being reconnected
---@param name string monitor name
---@param device Monitor monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device)
local is_used = false
if not engine.monitors then return false end
function renderer.handle_reconnect(name)
-- note: handle_resize is a more adaptive way of re-initializing a connected monitor
-- since it can handle a monitor being reconnected that isn't the right size
if engine.monitors.main_name == name then
is_used = true
engine.monitors.main = device
renderer.handle_resize(name)
elseif engine.monitors.flow_name == name then
is_used = true
engine.monitors.flow = device
renderer.handle_resize(name)
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
if monitor == name then
is_used = true
engine.monitors.unit_displays[idx] = device
renderer.handle_resize(name)
break
end
end
end
return is_used
end
-- handle a monitor being resized<br>
@@ -372,7 +342,7 @@ function renderer.handle_resize(name)
if not engine.monitors then return false, false end
if engine.monitors.main_name == name and engine.monitors.main then
if engine.monitors.main_iface == name and engine.monitors.main then
local device = engine.monitors.main ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect
@@ -390,7 +360,7 @@ function renderer.handle_resize(name)
ui.main_display = nil
end
iocontrol.fp_monitor_state("main", true)
iocontrol.fp_monitor_state("main", 2)
engine.dmesg_window.setVisible(not engine.ui_ready)
@@ -402,6 +372,8 @@ function renderer.handle_resize(name)
end)
if ok then
iocontrol.fp_monitor_state("main", 3)
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.main_display then
@@ -411,11 +383,10 @@ function renderer.handle_resize(name)
_print_too_small(device)
iocontrol.fp_monitor_state("main", false)
is_ok = false
end
else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then
elseif engine.monitors.flow_iface == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type Monitor
-- this is necessary if the bottom left block was broken and on reconnect
@@ -428,7 +399,7 @@ function renderer.handle_resize(name)
ui.flow_display = nil
end
iocontrol.fp_monitor_state("flow", true)
iocontrol.fp_monitor_state("flow", 2)
if engine.ui_ready then
local draw_start = util.time_ms()
@@ -438,6 +409,8 @@ function renderer.handle_resize(name)
end)
if ok then
iocontrol.fp_monitor_state("flow", 3)
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.flow_display then
@@ -447,12 +420,11 @@ function renderer.handle_resize(name)
_print_too_small(device)
iocontrol.fp_monitor_state("flow", false)
is_ok = false
end
end
else
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
for idx, monitor in ipairs(engine.monitors.unit_ifaces) do
local device = engine.monitors.unit_displays[idx]
if monitor == name and device then
@@ -466,7 +438,7 @@ function renderer.handle_resize(name)
ui.unit_displays[idx] = nil
end
iocontrol.fp_monitor_state(idx, true)
iocontrol.fp_monitor_state(idx, 2)
if engine.ui_ready then
local draw_start = util.time_ms()
@@ -476,6 +448,8 @@ function renderer.handle_resize(name)
end)
if ok then
iocontrol.fp_monitor_state(idx, 3)
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
else
if ui.unit_displays[idx] then
@@ -485,7 +459,6 @@ function renderer.handle_resize(name)
_print_too_small(device)
iocontrol.fp_monitor_state(idx, false)
is_ok = false
end
end
@@ -505,12 +478,12 @@ function renderer.handle_mouse(event)
if engine.fp_ready and event.monitor == "terminal" then
engine.ui.front_panel.handle_mouse(event)
elseif engine.ui_ready then
if event.monitor == engine.monitors.main_name then
if event.monitor == engine.monitors.main_iface then
if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
elseif event.monitor == engine.monitors.flow_name then
elseif event.monitor == engine.monitors.flow_iface then
if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end
else
for id, monitor in ipairs(engine.monitors.unit_name_map) do
for id, monitor in ipairs(engine.monitors.unit_ifaces) do
local display = engine.ui.unit_displays[id]
if event.monitor == monitor and display then
if display then display.handle_mouse(event) end

View File

@@ -69,7 +69,7 @@ end
-- PUBLIC FUNCTIONS --
-- initialize apisessions
---@param nic nic network interface
---@param nic nic API network interface
---@param config crd_config coordinator config
function apisessions.init(nic, config)
self.nic = nic

View File

@@ -12,6 +12,7 @@ local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local backplane = require("coordinator.backplane")
local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
@@ -19,7 +20,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.6.16"
local COORDINATOR_VERSION = "v1.7.0"
local CHUNK_LOAD_DELAY_S = 30.0
@@ -36,45 +37,13 @@ local log_crypto = coordinator.log_crypto
-- get configuration
----------------------------------------
-- mount connected devices (required for monitor setup)
ppm.mount_all()
local wait_on_load = true
local loaded, monitors = coordinator.load_config()
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
loaded, monitors = coordinator.load_config()
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if loaded ~= 0 then
-- first pass configuration check before validating monitors
if not coordinator.load_config() then
-- try to reconfigure (user action)
local success, error = configure.configure(loaded, monitors)
local success, error = configure.configure(1)
if success then
loaded, monitors = coordinator.load_config()
if loaded ~= 0 then
println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure")
if not coordinator.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
end
else
@@ -83,9 +52,6 @@ if loaded ~= 0 then
end
end
-- passed checks, good now
---@cast monitors monitors_struct
local config = coordinator.config
----------------------------------------
@@ -102,6 +68,65 @@ println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
crash.set_env("coordinator", COORDINATOR_VERSION)
crash.dbg_log_env()
----------------------------------------
-- display init
----------------------------------------
-- mount connected devices (required for monitor setup)
ppm.mount_all()
local wait_on_load = true
local disp_ok, disp_err = backplane.init_displays(config)
-- if the computer just started, its chunk may have just loaded (...or the user rebooted)
-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying
while wait_on_load and (not disp_ok) and os.clock() < CHUNK_LOAD_DELAY_S do
term.clear()
term.setCursorPos(1, 1)
println("There was a monitor configuration problem at boot.\n")
println("Startup will keep trying every 2s in case of chunk load delays.\n")
println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock())))
println("(click to skip to the configurator)")
local timer_id = util.start_timer(2)
while true do
local event, param1 = util.pull_event()
if event == "timer" and param1 == timer_id then
-- remount and re-attempt
ppm.mount_all()
disp_ok, disp_err = backplane.init_displays(config)
break
elseif event == "mouse_click" or event == "terminate" then
wait_on_load = false
break
end
end
end
if not disp_ok then
-- try to reconfigure (user action)
local success, error = configure.configure(2, disp_err)
if success then
if not coordinator.load_config() then
println("failed to load a valid configuration, please reconfigure")
return
else
disp_ok, disp_err = backplane.init_displays(config)
if not disp_ok then
println(disp_err)
println("please reconfigure")
return
end
end
else
println("configuration error: " .. error)
return
end
end
----------------------------------------
-- main application
----------------------------------------
@@ -111,16 +136,12 @@ local function main()
-- system startup
----------------------------------------
-- log mounts now since mounting was done before logging was ready
ppm.log_mounts()
-- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- report versions
iocontrol.fp_versions(COORDINATOR_VERSION, comms.version)
-- init renderer
renderer.configure(config)
renderer.set_displays(monitors)
renderer.init_displays()
renderer.init_displays(backplane.displays())
renderer.init_dmesg()
-- lets get started!
@@ -130,6 +151,12 @@ local function main()
log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION)
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms")
end
----------------------------------------
-- memory allocation
----------------------------------------
@@ -149,15 +176,9 @@ local function main()
shutdown = false
},
-- core coordinator devices
crd_dev = {
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker") ---@type Speaker|nil
},
-- system objects
---@class crd_sys
crd_sys = {
nic = nil, ---@type nic
coord_comms = nil, ---@type coord_comms
conn_watchdog = nil ---@type watchdog
},
@@ -165,68 +186,33 @@ local function main()
-- message queues
q = {
mq_render = mqueue.new()
},
-- message queue message types
q_types = {
MQ__RENDER_CMD = {
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
},
MQ__RENDER_DATA = {
MON_CONNECT = 1,
MON_DISCONNECT = 2,
MON_RESIZE = 3
}
}
}
local smem_dev = __shared_memory.crd_dev
local smem_sys = __shared_memory.crd_sys
local crd_state = __shared_memory.crd_state
----------------------------------------
-- setup alarm sounder subsystem
-- init system
----------------------------------------
if smem_dev.speaker == nil then
log_boot("annunciator alarm speaker not found")
println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found")
return
else
local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected")
sounder.init(smem_dev.speaker, config.SpeakerVolume)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end
-- modem and speaker initialization
if not backplane.init(config, __shared_memory) then return end
----------------------------------------
-- setup communications
----------------------------------------
-- message authentication init
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms")
end
-- get the communications modem
if smem_dev.modem == nil then
log_comms("wireless modem not found")
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
else
log_comms("wireless modem connected")
iocontrol.fp_has_modem(true)
end
-- create connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
smem_sys.conn_watchdog.cancel()
log.debug("startup> conn watchdog created")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
log.debug("startup> comms init")
log_comms("comms initialized")
----------------------------------------
-- start front panel
----------------------------------------
log_render("starting front panel UI...")
local fp_message
@@ -238,6 +224,16 @@ local function main()
return
else log_render("front panel ready") end
-- create connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
smem_sys.conn_watchdog.cancel()
log.debug("startup> conn watchdog created")
-- setup comms
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, backplane.active_nic(), backplane.wireless_nic(), smem_sys.conn_watchdog)
log.debug("startup> comms init")
log_comms("comms initialized")
----------------------------------------
-- start system
----------------------------------------

View File

@@ -4,6 +4,7 @@ local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local backplane = require("coordinator.backplane")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
@@ -20,19 +21,8 @@ local log_comms = coordinator.log_comms
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = {
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
}
local MQ__RENDER_DATA = {
MON_CONNECT = 1,
MON_DISCONNECT = 2,
MON_RESIZE = 3
}
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RENDER_SLEEP = 100 -- 100ms, 2 ticks
-- main thread
---@nodiscard
@@ -44,7 +34,7 @@ function threads.thread__main(smem)
-- execute thread
function public.exec()
iocontrol.fp_rt_status("main", true)
log.debug("main thread start")
log.debug("OS: main thread start")
local loop_clock = util.new_clock(MAIN_CLOCK)
@@ -55,10 +45,12 @@ function threads.thread__main(smem)
-- load in from shared memory
local crd_state = smem.crd_state
local nic = smem.crd_sys.nic
local coord_comms = smem.crd_sys.coord_comms
local conn_watchdog = smem.crd_sys.conn_watchdog
local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -66,66 +58,13 @@ function threads.thread__main(smem)
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only really care if this is our wireless modem
-- if it is another modem, handle other peripheral losses separately
if nic.is_modem(device) then
nic.disconnect()
log_sys("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log_sys("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
-- close out main UI
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- alert user to status
log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else
log_sys("non-comms modem disconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
elseif type == "speaker" then
---@cast device Speaker
log_sys("lost alarm sounder speaker")
iocontrol.fp_has_speaker(false)
end
backplane.detach(type, device, param1)
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
log_sys("comms modem reconnected")
nic.connect(device)
iocontrol.fp_has_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
elseif type == "speaker" then
---@cast device Speaker
log_sys("alarm sounder speaker reconnected")
sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end
backplane.attach(type, device, param1)
end
elseif event == "monitor_resize" then
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
@@ -137,7 +76,6 @@ function threads.thread__main(smem)
iocontrol.heartbeat()
-- maintain connection
if nic.is_connected() then
local ok, start_ui = coord_comms.try_connect()
if not ok then
crd_state.link_fail = true
@@ -149,7 +87,6 @@ function threads.thread__main(smem)
log_sys("supervisor connected, dispatching main UI start")
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
end
end
-- iterate sessions and free any closed ones
apisessions.iterate_all()
@@ -206,10 +143,10 @@ function threads.thread__main(smem)
-- check for termination request or UI crash
if event == "terminate" or ppm.should_terminate() then
crd_state.shutdown = true
log.info("terminate requested, main thread exiting")
log.info("OS: terminate requested, main thread exiting")
elseif not crd_state.ui_ok then
crd_state.shutdown = true
log.info("terminating due to fatal UI error")
log.info("OS: terminating due to fatal UI error")
end
if crd_state.shutdown then
@@ -247,7 +184,7 @@ function threads.thread__main(smem)
-- if status is true, then we are probably exiting, so this won't matter
-- this thread cannot be slept because it will miss events (namely "terminate")
if not crd_state.shutdown then
log.info("main thread restarting now...")
log.info("OS: main thread restarting now...")
end
end
end
@@ -265,12 +202,15 @@ function threads.thread__render(smem)
-- execute thread
function public.exec()
iocontrol.fp_rt_status("render", true)
log.debug("render thread start")
log.debug("OS: render thread start")
-- load in from shared memory
local crd_state = smem.crd_state
local render_queue = smem.q.mq_render
local MQ__RENDER_CMD = smem.q_types.MQ__RENDER_CMD
local MQ__RENDER_DATA = smem.q_types.MQ__RENDER_DATA
local last_update = util.time()
-- thread loop
@@ -317,18 +257,10 @@ function threads.thread__render(smem)
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
-- monitor connected
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
else
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
end
renderer.handle_reconnect(cmd.val)
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
-- monitor disconnected
if renderer.handle_disconnect(cmd.val) then
log_sys("lost a configured monitor")
else
log_sys("lost an unused monitor")
end
renderer.handle_disconnect(cmd.val)
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
-- monitor resized
local is_used, is_ok = renderer.handle_resize(cmd.val)
@@ -347,7 +279,7 @@ function threads.thread__render(smem)
-- check for termination request
if crd_state.shutdown then
log.info("render thread exiting")
log.info("OS: render thread exiting")
break
end
@@ -369,7 +301,7 @@ function threads.thread__render(smem)
iocontrol.fp_rt_status("render", false)
if not crd_state.shutdown then
log.info("render thread restarting in 5 seconds...")
log.info("OS: render thread restarting in 5 seconds...")
util.psleep(5)
end
end

View File

@@ -246,7 +246,7 @@ local function new_view(root, x, y)
-------------------------
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
mode.register(facility.ps, "process_mode", mode.set_value)

View File

@@ -486,7 +486,7 @@ local function init(parent, id)
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)

View File

@@ -17,6 +17,7 @@ local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local TabBar = require("graphics.elements.controls.TabBar")
@@ -30,13 +31,16 @@ local LINK_STATE = types.PANEL_LINK_STATE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local led_grn = style.led_grn
-- create new front panel view
---@param panel DisplayBox main displaybox
---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units)
---@param config crd_config configuration
local function init(panel, config)
local s_hi_box = style.fp_theme.highlight_box
local ps = iocontrol.get_db().fp.ps
local term_w, term_h = term.getSize()
@@ -60,7 +64,15 @@ local function init(panel, num_units)
heartbeat.register(ps, "heartbeat", heartbeat.update)
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=led_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=led_grn}
wd_modem.register(ps, "has_wd_modem", wd_modem.update)
wl_modem.register(ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=led_grn}
modem.register(ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
@@ -97,48 +109,44 @@ local function init(panel, num_units)
system.line_break()
modem.register(ps, "has_modem", modem.update)
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update)
system.line_break()
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
rt_main.register(ps, "routine__main", rt_main.update)
rt_render.register(ps, "routine__render", rt_render.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local hmi_devs = Div{parent=main_page,width=16,height=17,x=18,y=2}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local speaker = LED{parent=hmi_devs,label="SPEAKER",colors=led_grn}
speaker.register(ps, "has_speaker", speaker.update)
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
main_monitor.register(ps, "main_monitor", main_monitor.update)
hmi_devs.line_break()
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
local main_disp = LEDPair{parent=hmi_devs,label="MAIN DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
main_disp.register(ps, "main_monitor", main_disp.update)
monitors.line_break()
local flow_disp = LEDPair{parent=hmi_devs,label="FLOW DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
flow_disp.register(ps, "flow_monitor", flow_disp.update)
for i = 1, num_units do
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
hmi_devs.line_break()
for i = 1, config.UnitCount do
local unit_disp = LEDPair{parent=hmi_devs,label="UNIT "..i.." DISPLAY",off=style.fp_ind_bkg,c1=colors.red,c2=colors.green}
unit_disp.register(ps, "unit_monitor_" .. i, unit_disp.update)
end
--
-- about footer
-- hardware labeling
--
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
local hw_labels = Rectangle{parent=main_page,x=2,y=term_h-7,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("%03d", os.getComputerID())
TextBox{parent=hw_labels,text="FW "..ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-CRD",fg_bg=s_hi_box}
--
-- page handling

View File

@@ -102,7 +102,7 @@ return function (args)
end
-- set the value
---@param val integer new value
---@param val boolean new value
function e.set_value(val)
e.value = val
draw()

View File

@@ -63,7 +63,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
@@ -78,10 +78,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_units()
tmp_cfg.TempScale = temp_scale.get_value()
@@ -216,7 +216,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}

View File

@@ -491,21 +491,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
function public.close_sv()
sv_watchdog.cancel()
nav.unload_sv()
self.sv.linked = false
self.sv.r_seq_num = nil
self.sv.addr = comms.BROADCAST
if self.sv.linked then
self.sv.linked = false
_send_sv(MGMT_TYPE.CLOSE, {})
end
end
-- close connection to coordinator API server
function public.close_api()
api_watchdog.cancel()
nav.unload_api()
self.api.linked = false
self.api.r_seq_num = nil
self.api.addr = comms.BROADCAST
if self.api.linked then
self.api.linked = false
_send_crd(MGMT_TYPE.CLOSE, {})
end
end
-- close the connections to the servers
function public.close()
@@ -515,24 +523,18 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
-- attempt to re-link if any of the dependent links aren't active
function public.link_update()
if not self.sv.linked then
if not (self.sv.linked and self.api.linked) then
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY, false, nil)
elseif self.sv.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
else
iocontrol.report_link_state(LINK_STATE.UNLINKED, false, false)
end
if self.establish_delay_counter <= 0 then
_send_sv_establish()
self.establish_delay_counter = 4
else
self.establish_delay_counter = self.establish_delay_counter - 1
end
elseif not self.api.linked then
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, nil, false)
if self.establish_delay_counter <= 0 then
_send_api_establish()
if not self.api.linked then _send_api_establish() end
if not self.sv.linked then _send_sv_establish() end
self.establish_delay_counter = 4
else
self.establish_delay_counter = self.establish_delay_counter - 1

View File

@@ -22,7 +22,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v1.0.3"
local POCKET_VERSION = "v1.0.4"
local println = util.println
local println_ts = util.println_ts

View File

@@ -11,8 +11,8 @@ local core = require("graphics.core")
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RENDER_SLEEP = 100 -- 100ms, 2 ticks
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA

View File

@@ -163,7 +163,7 @@ local function new_view(root)
TextBox{parent=info_div,x=2,y=1,text="This app provides tools to test alarm sounds by alarm and by tone (1-8)."}
TextBox{parent=info_div,x=2,y=6,text="The system must be idle (all units stopped with no alarms active) for testing to run."}
TextBox{parent=info_div,x=2,y=12,text="Currently, testing will be denied unless you have a Facility Authentication Key set (this will change in the future)."}
TextBox{parent=info_div,x=2,y=12,text="Testing will be denied unless you enabled it in the Supervisor's configuration."}
--#endregion

View File

@@ -162,7 +162,7 @@ local function new_view(root)
TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER}
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
mode.register(f_ps, "process_mode", mode.set_value)

324
reactor-plc/backplane.lua Normal file
View File

@@ -0,0 +1,324 @@
--
-- Reactor PLC System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local println = util.println
---@class plc_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type plc_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic
wd_nic = nil, ---@type nic|nil
wl_nic = nil ---@type nic|nil
}
-- initialize the system peripheral backplane<br>
---@param config plc_config
---@param __shared_memory plc_shared_memory
--- EVENT_CONSUMER: this function consumes events
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
local plc_dev = __shared_memory.plc_dev
local plc_state = __shared_memory.plc_state
plc_state.degraded = false
-- Modem Init
if _bp.smem.networked then
-- init wired NIC
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
_bp.wd_nic = wd_nic
_bp.act_nic = wd_nic -- set this as active for now
wd_nic.closeAll()
wd_nic.open(config.PLC_Channel)
plc_state.wd_modem = wd_nic.is_connected()
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
-- set this as active if connected or if both modems are disconnected and this is preferred
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
_bp.act_nic = wl_nic
log.info("BKPLN: switched active to preferred wireless")
end
_bp.wl_nic = wl_nic
wl_nic.closeAll()
wl_nic.open(config.PLC_Channel)
plc_state.wl_modem = wl_nic.is_connected()
end
-- comms modem is required if networked
if not (plc_state.wd_modem or plc_state.wl_modem) then
println("startup> no comms modem found")
log.warning("BKPLN: no comms modem on startup")
plc_state.degraded = true
end
end
-- Reactor Init
---@diagnostic disable-next-line: assign-type-mismatch
plc_dev.reactor = ppm.get_fission_reactor()
plc_state.no_reactor = plc_dev.reactor == nil
-- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then
log.info("BKPLN: REACTOR LINK_DOWN")
println("startup> fission reactor not found")
log.warning("BKPLN: no reactor on startup")
plc_state.degraded = true
plc_state.reactor_formed = false
-- mount a virtual peripheral to init the RPS with
local _, dev = ppm.mount_virtual()
plc_dev.reactor = dev
log.info("BKPLN: mounted virtual device as reactor")
else
log.info("BKPLN: REACTOR LINK_UP " .. ppm.get_iface(plc_dev.reactor))
if not plc_dev.reactor.isFormed() then
println("startup> fission reactor is not formed")
log.warning("BKPLN: reactor logic adapter detected, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
end
end
end
-- get the active NIC
function backplane.active_nic() return _bp.act_nic end
-- handle a backplane peripheral attach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.attach(iface, type, device, print_no_fp)
local MQ__RPS_CMD = _bp.smem.q_types.MQ__RPS_CMD
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local networked = _bp.smem.networked
local state = _bp.smem.plc_state
local dev = _bp.smem.plc_dev
local sys = _bp.smem.plc_sys
if type ~= nil and device ~= nil then
if state.no_reactor and (type == "fissionReactorLogicAdapter") then
-- reconnected reactor
log.info("BKPLN: REACTOR LINK_UP " .. iface)
dev.reactor = device
state.no_reactor = false
print_no_fp("reactor connected")
log.info("BKPLN: reactor connected")
-- we need to assume formed here as we cannot check in this main loop
-- RPS will identify if it isn't and this will get set false later
state.reactor_formed = true
-- determine if we are still in a degraded state
if ((not networked) or (state.wd_modem or state.wl_modem)) and state.reactor_formed then
state.degraded = false
end
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
sys.rps.reconnect_reactor(dev.reactor)
if networked then
sys.plc_comms.reconnect_reactor(dev.reactor)
end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
sys.rps.reset_reattach()
elseif networked and type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
print_no_fp("wired comms modem connected")
state.wd_modem = true
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.act_nic = wd_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem connected")
state.wl_modem = true
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.act_nic = wl_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
print_no_fp("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
-- determine if we are still in a degraded state
if (state.wd_modem or state.wl_modem) and state.reactor_formed and not state.no_reactor then
state.degraded = false
end
end
end
end
-- handle a backplane peripheral detach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.detach(iface, type, device, print_no_fp)
local MQ__RPS_CMD = _bp.smem.q_types.MQ__RPS_CMD
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local state = _bp.smem.plc_state
local dev = _bp.smem.plc_dev
local sys = _bp.smem.plc_sys
if device == dev.reactor then
log.info("BKPLN: REACTOR LINK_DOWN " .. iface)
print_no_fp("reactor disconnected")
log.warning("BKPLN: reactor disconnected")
state.no_reactor = true
state.degraded = true
elseif _bp.smem.networked and type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
if wd_nic and wd_nic.is_modem(device) then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
state.wd_modem = false
elseif wl_nic and wl_nic.is_modem(device) then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
state.wl_modem = false
end
-- we only care if this is our active comms modem
if _bp.act_nic.is_modem(device) then
print_no_fp("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.act_nic == wl_nic then
-- wireless active disconnected
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if wl_nic and modem then
log.info("BKPLN: found another wireless modem, using it for comms")
wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
state.wl_modem = true
elseif wd_nic and wd_nic.is_connected() then
_bp.act_nic = wd_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem")
else
-- no other wireless modems, wired unavailable
state.degraded = true
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
elseif wl_nic and wl_nic.is_connected() then
-- wired active disconnected, wireless available
_bp.act_nic = wl_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem")
else
-- wired active disconnected, wireless unavailable
state.degraded = true
_bp.smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
elseif wd_nic and wd_nic.is_modem(device) then
-- wired, but not active
print_no_fp("standby wired modem disconnected")
log.info("BKPLN: standby wired modem disconnected")
elseif wl_nic and wl_nic.is_modem(device) then
-- wireless, but not active
print_no_fp("standby wireless modem disconnected")
log.info("BKPLN: standby wireless modem disconnected")
else
print_no_fp("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
end
end
return backplane

View File

@@ -24,10 +24,12 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
checking_wl = true,
wd_modem = nil, ---@type Modem|nil
wl_modem = nil, ---@type Modem|nil
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
self_check_pass = true,
@@ -48,7 +50,7 @@ local function check_complete()
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
end
-- send a management packet to the supervisor
-- send a management packet to the supervisor (one-time broadcast)
---@param msg_type MGMT_TYPE
---@param msg table
local function send_sv(msg_type, msg)
@@ -56,10 +58,9 @@ local function send_sv(msg_type, msg)
local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
s_pkt.make(comms.BROADCAST, util.time_ms() * 10, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.settings.SVR_Channel, self.settings.PLC_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
@@ -75,10 +76,7 @@ local function handle_packet(packet)
local est_ack = packet.data[1]
if est_ack== ESTABLISH_ACK.ALLOW then
self.self_check_msg(nil, true, "")
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
if self.self_check_pass then check_complete() end
-- success
elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied"
elseif est_ack == ESTABLISH_ACK.COLLISION then
@@ -97,18 +95,20 @@ local function handle_packet(packet)
end
self.net_listen = false
self.run_test_btn.enable()
if error_msg then
self.self_check_msg(nil, false, error_msg)
else
self.self_check_msg(nil, true, "")
end
util.push_event("conn_test_complete", error_msg == nil)
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.run_test_btn.enable()
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
util.push_event("conn_test_complete", false)
end
-- execute the self-check
@@ -121,12 +121,20 @@ local function self_check()
self.self_check_pass = true
local cfg = self.settings
local modem = ppm.get_wireless_modem()
self.wd_modem = ppm.get_modem(cfg.WiredModem)
self.wl_modem = ppm.get_wireless_modem()
local reactor = ppm.get_fission_reactor()
local valid_cfg = plc.validate_config(cfg)
-- check for comms modems
if cfg.Networked then
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
if cfg.WiredModem then
self.self_check_msg("> check wired comms modem connected...", self.wd_modem, "please connect the wired comms modem " .. cfg.WiredModem)
end
if cfg.WirelessModem then
self.self_check_msg("> check wireless/ender modem connected...", self.wl_modem, "please connect an ender or wireless modem for wireless comms")
end
end
self.self_check_msg("> check fission reactor connected...", reactor ~= nil, "please connect the reactor PLC to the reactor's fission reactor logic adapter")
@@ -136,8 +144,11 @@ local function self_check()
self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones")
if cfg.Networked and valid_cfg and modem then
self.self_check_msg("> check supervisor connection...")
if cfg.Networked and valid_cfg then
self.checking_wl = true
if cfg.WirelessModem and self.wl_modem then
self.self_check_msg("> check wireless supervisor connection...")
-- init mac as needed
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
@@ -146,17 +157,24 @@ local function self_check()
network.deinit_mac()
end
self.nic = network.nic(modem)
comms.set_trusted_range(cfg.TrustedRange)
self.nic = network.nic(self.wl_modem)
self.nic.closeAll()
self.nic.open(cfg.PLC_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, cfg.UnitID })
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.PLC, cfg.UnitID })
tcd.dispatch_unique(8, handle_timeout)
elseif cfg.WiredModem and self.wd_modem then
-- skip to wired
util.push_event("conn_test_complete", true)
else
self.self_check_msg("> no modem, can't test supervisor connection", false)
end
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
@@ -240,4 +258,44 @@ function check.receive_sv(side, sender, reply_to, message, distance)
end
end
-- handle completed connection tests
---@param pass boolean
function check.conn_test_callback(pass)
local cfg = self.settings
if self.checking_wl then
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wireless interface, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
end
if cfg.WiredModem and self.wd_modem then
self.checking_wl = false
self.self_check_msg("> check wired supervisor connection...")
comms.set_trusted_range(0)
self.nic = network.nic(self.wd_modem)
self.nic.closeAll()
self.nic.open(cfg.PLC_Channel)
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.PLC, cfg.UnitID })
tcd.dispatch_unique(8, handle_timeout)
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
else
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wired interface, the wire is intact, and your channels are correct")
end
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
end
return check

View File

@@ -1,4 +1,5 @@
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
@@ -20,6 +21,8 @@ local TextField = require("graphics.elements.form.TextField")
local IndLight = require("graphics.elements.indicators.IndicatorLight")
local tri = util.trinary
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
@@ -30,6 +33,10 @@ local self = {
set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function
wireless = nil, ---@type Checkbox
wl_pref = nil, ---@type Checkbox
wired = nil, ---@type Checkbox
range = nil, ---@type NumberField
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
@@ -154,14 +161,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
function self.bundled_emcool(en) if en then color.enable() else color.disable() end end
TextBox{parent=plc_c_5,x=1,y=1,height=5,text="Advanced Options"}
local invert = Checkbox{parent=plc_c_5,x=1,y=3,label="Invert",default=ini_cfg.EmerCoolInvert,box_fg_bg=cpair(colors.orange,colors.black),callback=function()end}
TextBox{parent=plc_c_5,x=10,y=3,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local invert = Checkbox{parent=plc_c_5,x=1,y=3,label="Invert",default=ini_cfg.EmerCoolInvert,box_fg_bg=cpair(colors.orange,colors.black)}
TextBox{parent=plc_c_5,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=plc_c_5,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
tmp_cfg.EmerCoolColor = tri(bundled.get_value(), color_options_map[color.get_value()], nil)
tmp_cfg.EmerCoolInvert = invert.get_value()
next_from_plc()
end
@@ -177,22 +183,88 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
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="PLC Channel"}
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
local function en_dis_pref()
if self.wireless.get_value() and self.wired.get_value() then
self.wl_pref.enable()
else
self.wl_pref.set_value(self.wireless.get_value())
self.wl_pref.disable()
end
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 on_wired_change(_)
en_dis_pref()
tool_ctl.gen_modem_list()
end
self.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=en_dis_pref}
self.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}
self.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="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",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}
en_dis_pref()
local function submit_interfaces()
tmp_cfg.WirelessModem = self.wireless.get_value()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PreferWireless = self.wl_pref.get_value()
else
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
self.wl_pref.set_value(tmp_cfg.PreferWireless)
end
if not self.wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (self.wired.get_value() or self.wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif self.wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
if tmp_cfg.WirelessModem then
self.range.enable()
else
self.range.set_value(0)
self.range.disable()
end
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="PLC Channel"}
local plc_chan = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=9,y=12,height=4,text="[PLC_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())
@@ -200,7 +272,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if svr_c ~= nil and plc_c ~= nil then
tmp_cfg.SVR_Channel = svr_c
tmp_cfg.PLC_Channel = plc_c
net_pane.set_value(2)
net_pane.set_value(3)
chan_err.hide(true)
elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.")
@@ -211,54 +283,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
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)"}
self.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,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents wireless 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 n3_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())
local range_val = tonumber(range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = range_val
net_pane.set_value(3)
p2_err.hide(true)
elseif timeout_val == nil then
p2_err.set_value("Please set the connection timeout.")
p2_err.show()
local range_val = tonumber(self.range.get_value())
if timeout_val == nil then
n3_err.set_value("Please set the connection timeout.")
n3_err.show()
elseif tmp_cfg.WirelessModem and (range_val == nil) then
n3_err.set_value("Please set the trusted range.")
n3_err.show()
else
p2_err.set_value("Please set the trusted range.")
p2_err.show()
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = tri(tmp_cfg.WirelessModem, range_val, 0)
if tmp_cfg.WirelessModem then
net_pane.set_value(4)
else
main_pane.set_value(4)
tmp_cfg.AuthKey = ""
end
n3_err.hide(true)
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 for 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(util.trinary(enable, "*", nil)) end
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()
@@ -269,8 +349,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
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
@@ -283,7 +363,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -329,7 +409,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
@@ -368,7 +448,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function back_from_colors()
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4))
tool_ctl.jumped_to_color = false
recolor(1)
end
@@ -471,10 +551,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
try_set(bundled, ini_cfg.EmerCoolColor ~= nil)
if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end
try_set(invert, ini_cfg.EmerCoolInvert)
try_set(self.wireless, ini_cfg.WirelessModem)
try_set(self.wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(timeout, ini_cfg.ConnTimeout)
try_set(range, ini_cfg.TrustedRange)
try_set(self.range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
@@ -591,7 +674,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local val = util.strval(raw)
if f[1] == "AuthKey" and raw then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw))
@@ -601,7 +684,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if (string.len(val) > val_max_w) or string.find(val, "\n") then
@@ -623,6 +706,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list()
modem_list.remove_all()
local enable = self.wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
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 end
if 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

@@ -3,6 +3,7 @@
--
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
@@ -33,7 +34,8 @@ local changes = {
{ "v1.6.8", { "ConnTimeout can now have a fractional part" } },
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.8.21", { "Added option to invert emergency coolant redstone control" } }
{ "v1.8.21", { "Added option to invert emergency coolant redstone control" } },
{ "v1.10.0", { "Added support for wired communications modems" } }
}
---@class plc_configurator
@@ -68,6 +70,8 @@ local tool_ctl = {
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
gen_modem_list = function () end
}
---@class plc_config
@@ -78,6 +82,9 @@ local tmp_cfg = {
EmerCoolSide = nil, ---@type string|nil
EmerCoolColor = nil, ---@type color|nil
EmerCoolInvert = false, ---@type boolean
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
@@ -103,6 +110,9 @@ local fields = {
{ "EmerCoolSide", "Emergency Coolant Side", nil },
{ "EmerCoolColor", "Emergency Coolant Color", nil },
{ "EmerCoolInvert", "Emergency Coolant Invert", false },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 },
{ "ConnTimeout", "Connection Timeout", 5 },
@@ -261,8 +271,13 @@ 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.WiredModem = ini_cfg.WiredModem
reset_term()
ppm.mount_all()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
@@ -272,6 +287,8 @@ function configurator.configure(ask_config)
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_modem_list()
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -288,6 +305,16 @@ function configurator.configure(ask_config)
display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
elseif event == "conn_test_complete" then
check.conn_test_callback(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_modem_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_modem_list()
end
if event == "terminate" then return end

View File

@@ -33,7 +33,7 @@ function databus.rps_scram() dbus_iface.rps_scram() end
-- transmit a command to the RPS to reset
function databus.rps_reset() dbus_iface.rps_reset() end
-- transmit firmware versions across the bus
-- transmit firmware versions
---@param plc_v string PLC version
---@param comms_v string comms version
function databus.tx_versions(plc_v, comms_v)
@@ -41,18 +41,19 @@ function databus.tx_versions(plc_v, comms_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit unit ID across the bus
-- transmit unit ID
---@param id integer unit ID
function databus.tx_id(id)
databus.ps.publish("unit_id", id)
end
-- transmit hardware status across the bus
-- transmit hardware status
---@param plc_state plc_state
function databus.tx_hw_status(plc_state)
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
databus.ps.publish("has_modem", not plc_state.no_modem)
databus.ps.publish("degraded", plc_state.degraded)
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
databus.ps.publish("has_wd_modem", plc_state.wd_modem)
databus.ps.publish("has_wl_modem", plc_state.wl_modem)
end
-- transmit thread (routine) statuses
@@ -62,19 +63,19 @@ function databus.tx_rt_status(thread, ok)
databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
-- transmit supervisor link state
---@param state integer
function databus.tx_link_state(state)
databus.ps.publish("link_state", state)
end
-- transmit reactor enable state across the bus
-- transmit reactor enable state
---@param active any reactor active
function databus.tx_reactor_state(active)
databus.ps.publish("reactor_active", active == true)
end
-- transmit RPS data across the bus
-- transmit RPS data
---@param tripped boolean RPS tripped
---@param status boolean[] RPS status
---@param emer_cool_active boolean RPS activated the emergency coolant
@@ -94,11 +95,4 @@ function databus.tx_rps(tripped, status, emer_cool_active)
databus.ps.publish("emer_cool", emer_cool_active)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

View File

@@ -35,12 +35,11 @@ local ind_red = style.ind_red
-- create new front panel view
---@param panel DisplayBox main displaybox
local function init(panel)
---@param config plc_config configuraiton
local function init(panel, config)
local s_hi_box = style.theme.highlight_box
local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
local term_w, _ = term.getSize()
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
@@ -59,7 +58,21 @@ local function init(panel)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
if config.Networked then
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=ind_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=ind_grn}
wd_modem.register(databus.ps, "has_wd_modem", wd_modem.update)
wl_modem.register(databus.ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
modem.register(databus.ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
else
local _ = LED{parent=system,label="MODEM",colors=ind_grn}
end
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
@@ -99,9 +112,6 @@ local function init(panel)
system.line_break()
reactor.register(databus.ps, "reactor_dev_state", reactor.update)
modem.register(databus.ps, "has_modem", modem.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
local rt_rps = LED{parent=system,label="RT RPS",colors=ind_grn}
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=ind_grn}
@@ -115,12 +125,8 @@ local function init(panel)
rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=5,width=6,text=comp_id,fg_bg=disabled_fg}
--
-- status & controls
-- status & controls & hardware labeling
--
local status = Div{parent=panel,width=term_w-32,height=18,x=17,y=3}
@@ -146,16 +152,14 @@ local function init(panel)
active.register(databus.ps, "reactor_active", active.update)
scram.register(databus.ps, "rps_scram", scram.update)
--
-- about footer
--
local hw_labels = Rectangle{parent=status,width=status.get_width()-2,height=5,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("%03d", os.getComputerID())
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
TextBox{parent=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-PLC",fg_bg=s_hi_box}
--
-- rps list

View File

@@ -45,6 +45,9 @@ function plc.load_config()
config.EmerCoolColor = settings.get("EmerCoolColor")
config.EmerCoolInvert = settings.get("EmerCoolInvert")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
@@ -70,7 +73,11 @@ function plc.validate_config(cfg)
cfv.assert_type_int(cfg.UnitID)
cfv.assert_type_bool(cfg.EmerCoolEnable)
if cfg.Networked == true then
if cfg.Networked then
cfv.assert_type_bool(cfg.WirelessModem)
cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string"))
cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string"))
cfv.assert_type_bool(cfg.PreferWireless)
cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.PLC_Channel)
cfv.assert_type_num(cfg.ConnTimeout)
@@ -552,13 +559,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
max_burn_rate = nil
}
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
end
-- PRIVATE FUNCTIONS --
-- configure network channels
nic.closeAll()
nic.open(config.PLC_Channel)
--#region PRIVATE FUNCTIONS --
-- send an RPLC packet
---@param msg_type RPLC_TYPE
@@ -816,11 +821,20 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
end
end
-- PUBLIC FUNCTIONS --
--#endregion
--#region PUBLIC FUNCTIONS --
---@class plc_comms
local public = {}
-- switch the current active NIC
---@param act_nic nic
function public.switch_nic(act_nic)
public.close()
nic = act_nic
end
-- reconnect a newly connected reactor
---@param new_reactor table
function public.reconnect_reactor(new_reactor)
@@ -1098,6 +1112,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
---@nodiscard
function public.is_linked() return self.linked end
--#endregion
return public
end

View File

@@ -18,15 +18,14 @@ local ui = {
}
-- try to start the UI
---@param theme FP_THEME front panel theme
---@param color_mode COLOR_MODE color mode
---@param config plc_config configuration
---@return boolean success, any error_msg
function renderer.try_start_ui(theme, color_mode)
function renderer.try_start_ui(config)
local status, msg = true, nil
if ui.display == nil then
-- set theme
style.set_theme(theme, color_mode)
style.set_theme(config.FrontPanelTheme, config.ColorMode)
-- reset terminal
term.setTextColor(colors.white)
@@ -40,7 +39,7 @@ function renderer.try_start_ui(theme, color_mode)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[color_mode]
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
@@ -48,7 +47,7 @@ function renderer.try_start_ui(theme, color_mode)
-- init front panel view
status, msg = pcall(function ()
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(ui.display)
panel_view(ui.display, config)
end)
if status then

View File

@@ -3,6 +3,7 @@
--
require("/initenv").init_env()
local backplane = require("reactor-plc.backplane")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
@@ -18,7 +19,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.9.0"
local R_PLC_VERSION = "v1.10.0"
local println = util.println
local println_ts = util.println_ts
@@ -66,7 +67,7 @@ local function main()
-- startup
----------------------------------------
-- record firmware versions and ID
-- report versions and ID
databus.tx_versions(R_PLC_VERSION, comms.version)
databus.tx_id(config.UnitID)
@@ -90,9 +91,10 @@ local function main()
fp_ok = false,
shutdown = false,
degraded = true,
reactor_formed = true,
no_reactor = true,
no_modem = true
reactor_formed = true,
wd_modem = false,
wl_modem = false
},
-- control setpoints
@@ -102,19 +104,16 @@ local function main()
burn_rate = 0.0
},
-- core PLC devices
-- global PLC devices, still initialized by the backplane
---@class plc_dev
plc_dev = {
---@diagnostic disable-next-line: assign-type-mismatch
reactor = ppm.get_fission_reactor(), ---@type table
modem = ppm.get_wireless_modem()
reactor = nil ---@type table
},
-- system objects
---@class plc_sys
plc_sys = {
rps = nil, ---@type rps
nic = nil, ---@type nic
plc_comms = nil, ---@type plc_comms
conn_watchdog = nil ---@type watchdog
},
@@ -124,6 +123,18 @@ local function main()
mq_rps = mqueue.new(),
mq_comms_tx = mqueue.new(),
mq_comms_rx = mqueue.new()
},
-- message queue message types
q_types = {
MQ__RPS_CMD = {
SCRAM = 1,
DEGRADED_SCRAM = 2,
TRIP_TIMEOUT = 3
},
MQ__COMM_CMD = {
SEND_STATUS = 1
}
}
}
@@ -132,43 +143,8 @@ local function main()
local plc_state = __shared_memory.plc_state
-- initial state evaluation
plc_state.no_reactor = smem_dev.reactor == nil
plc_state.no_modem = smem_dev.modem == nil
-- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then
println("startup> fission reactor not found")
log.warning("startup> no reactor on startup")
plc_state.degraded = true
plc_state.reactor_formed = false
-- mount a virtual peripheral to init the RPS with
local _, dev = ppm.mount_virtual()
smem_dev.reactor = dev
log.info("startup> mounted virtual device as reactor")
elseif not smem_dev.reactor.isFormed() then
println("startup> fission reactor is not formed")
log.warning("startup> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
end
-- modem is required if networked
if __shared_memory.networked and plc_state.no_modem then
println("startup> wireless modem not found")
log.warning("startup> no wireless modem on startup")
-- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram()
end
plc_state.degraded = true
end
-- reactor and modem initialization
backplane.init(config, __shared_memory)
-- scram on boot if networked, otherwise leave the reactor be
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
@@ -178,7 +154,7 @@ local function main()
-- setup front panel
local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
plc_state.fp_ok, message = renderer.try_start_ui(config)
-- ...or not
if not plc_state.fp_ok then
@@ -212,8 +188,7 @@ local function main()
log.debug("startup> conn watchdog started")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, backplane.active_nic(), smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("startup> comms init")
else
_println_no_fp("startup> starting in non-networked mode")

View File

@@ -4,6 +4,7 @@ local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local backplane = require("reactor-plc.backplane")
local databus = require("reactor-plc.databus")
local renderer = require("reactor-plc.renderer")
@@ -11,23 +12,13 @@ local core = require("graphics.core")
local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks)
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RPS_SLEEP = 250 -- 250ms, 5 ticks
local COMMS_SLEEP = 150 -- 150ms, 3 ticks
local SP_CTRL_SLEEP = 250 -- 250ms, 5 ticks
local BURN_RATE_RAMP_mB_s = 5.0
local MQ__RPS_CMD = {
SCRAM = 1,
DEGRADED_SCRAM = 2,
TRIP_TIMEOUT = 3
}
local MQ__COMM_CMD = {
SEND_STATUS = 1
}
-- main thread
---@nodiscard
---@param smem plc_shared_memory
@@ -43,28 +34,27 @@ function threads.thread__main(smem)
databus.tx_rt_status("main", true)
log.debug("OS: main thread start")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
-- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks)
local LINK_TICKS = 8
local LINK_TICKS = 2
local ticks_to_update = 0
local loop_clock = util.new_clock(MAIN_CLOCK)
-- load in from shared memory
local networked = smem.networked
local plc_state = smem.plc_state
local plc_dev = smem.plc_dev
local rps = smem.plc_sys.rps
local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog
local MQ__RPS_CMD = smem.q_types.MQ__RPS_CMD
local MQ__COMM_CMD = smem.q_types.MQ__COMM_CMD
-- start clock
loop_clock.start()
-- event loop
while true do
-- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps
local nic = smem.plc_sys.nic
local plc_comms = smem.plc_sys.plc_comms
local conn_watchdog = smem.plc_sys.conn_watchdog
local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event
@@ -76,10 +66,10 @@ function threads.thread__main(smem)
loop_clock.start()
-- send updated data
if networked and nic.is_connected() then
if networked then
if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
else
elseif backplane.active_nic().is_connected() then
if ticks_to_update == 0 then
plc_comms.send_link_req()
ticks_to_update = LINK_TICKS
@@ -101,7 +91,7 @@ function threads.thread__main(smem)
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- determine if we are still in a degraded state
if (not networked) or nic.is_connected() then
if (not networked) or backplane.active_nic().is_connected() then
plc_state.degraded = false
end
@@ -119,7 +109,7 @@ function threads.thread__main(smem)
-- update indicators
databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and nic.is_connected() then
elseif event == "modem_message" and networked then
-- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then
@@ -136,38 +126,8 @@ function threads.thread__main(smem)
elseif event == "peripheral_detach" then
-- peripheral disconnect
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if device == plc_dev.reactor then
println_ts("reactor disconnected!")
log.error("reactor logic adapter disconnected")
plc_state.no_reactor = true
plc_state.degraded = true
elseif networked and type == "modem" then
---@cast device Modem
-- we only care if this is our wireless modem
if nic.is_modem(device) then
nic.disconnect()
println_ts("comms modem disconnected!")
log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
plc_state.no_modem = true
plc_state.degraded = true
-- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
else
log.warning("a modem was disconnected")
end
end
backplane.detach(param1, type, device, println_ts)
end
-- update indicators
@@ -175,58 +135,8 @@ function threads.thread__main(smem)
elseif event == "peripheral" then
-- peripheral connect
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if plc_state.no_reactor and (type == "fissionReactorLogicAdapter") then
-- reconnected reactor
plc_dev.reactor = device
plc_state.no_reactor = false
println_ts("reactor reconnected")
log.info("reactor reconnected")
-- we need to assume formed here as we cannot check in this main loop
-- RPS will identify if it isn't and this will get set false later
plc_state.reactor_formed = true
-- determine if we are still in a degraded state
if (not networked or not plc_state.no_modem) and plc_state.reactor_formed then
plc_state.degraded = false
end
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
rps.reconnect_reactor(plc_dev.reactor)
if networked then
plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_reattach()
elseif networked and type == "modem" then
---@cast device Modem
-- note, check init_ok first since nic will be nil if it is false
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
plc_dev.modem = device
plc_state.no_modem = false
nic.connect(device)
println_ts("comms modem reconnected")
log.info("comms modem reconnected")
-- determine if we are still in a degraded state
if plc_state.reactor_formed and not plc_state.no_reactor then
plc_state.degraded = false
end
elseif device.isWireless() then
log.info("unused wireless modem connected")
else
log.info("wired modem connected")
end
end
backplane.attach(param1, type, device, println_ts)
end
-- update indicators
@@ -293,6 +203,8 @@ function threads.thread__rps(smem)
local rps_queue = smem.q.mq_rps
local MQ__RPS_CMD = smem.q_types.MQ__RPS_CMD
local was_linked = false
local last_update = util.time()
@@ -426,6 +338,8 @@ function threads.thread__comms_tx(smem)
local plc_state = smem.plc_state
local comms_queue = smem.q.mq_comms_tx
local MQ__COMM_CMD = smem.q_types.MQ__COMM_CMD
local last_update = util.time()
-- thread loop

275
rtu/backplane.lua Normal file
View File

@@ -0,0 +1,275 @@
--
-- RTU Gateway System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local databus = require("rtu.databus")
local rtu = require("rtu.rtu")
local println = util.println
---@class rtu_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type rtu_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic
wd_nic = nil, ---@type nic|nil
wl_nic = nil, ---@type nic|nil
sounders = {} ---@type rtu_speaker_sounder[]
}
-- initialize the system peripheral backplane
---@param config rtu_config
---@param __shared_memory rtu_shared_memory
---@return boolean success
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
-- Modem Init
-- init wired NIC
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
_bp.wd_nic = wd_nic
_bp.act_nic = wd_nic -- set this as active for now
wd_nic.closeAll()
wd_nic.open(config.RTU_Channel)
databus.tx_hw_wd_modem(modem ~= nil)
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
-- set this as active if connected or if both modems are disconnected and this is preferred
if (modem and _bp.wlan_pref) or not (_bp.act_nic and _bp.act_nic.is_connected()) then
_bp.act_nic = wl_nic
end
_bp.wl_nic = wl_nic
wl_nic.closeAll()
wl_nic.open(config.RTU_Channel)
databus.tx_hw_wl_modem(modem ~= nil)
end
-- at least one comms modem is required
if not ((_bp.wd_nic and _bp.wd_nic.is_connected()) or (_bp.wl_nic and _bp.wl_nic.is_connected())) then
println("startup> no comms modem found")
log.warning("BKPLN: no comms modem on startup")
return false
end
-- Speaker Init
-- find and setup all speakers
local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do
log.info("BKPLN: SPEAKER LINK_UP " .. ppm.get_iface(s))
local sounder = rtu.init_sounder(s)
table.insert(_bp.sounders, sounder)
log.debug(util.c("BKPLN: added speaker sounder, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#_bp.sounders)
return true
end
-- get the active NIC
function backplane.active_nic() return _bp.act_nic end
-- get the sounder interfaces
function backplane.sounders() return _bp.sounders end
-- handle a backplane peripheral attach
---@param type string
---@param device table
---@param iface string
---@param print_no_fp function
function backplane.attach(type, device, iface, print_no_fp)
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.rtu_sys.rtu_comms
if type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
print_no_fp("wired comms modem reconnected")
databus.tx_hw_wd_modem(true)
if (_bp.act_nic ~= wd_nic) and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.act_nic = wd_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif wl_nic and (not wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem reconnected")
databus.tx_hw_wl_modem(true)
if (_bp.act_nic ~= wl_nic) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.act_nic = wl_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
print_no_fp("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_UP " .. iface)
table.insert(_bp.sounders, rtu.init_sounder(device))
print_no_fp("a speaker was connected")
log.info("BKPLN: setup speaker sounder for speaker " .. iface)
databus.tx_hw_spkr_count(#_bp.sounders)
end
end
-- handle a backplane peripheral detach
---@param type string
---@param device table
---@param iface string
---@param print_no_fp function
function backplane.detach(type, device, iface, print_no_fp)
local wl_nic, wd_nic = _bp.wl_nic, _bp.wd_nic
local comms = _bp.smem.rtu_sys.rtu_comms
if type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
if wd_nic and wd_nic.is_modem(device) then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
databus.tx_hw_wd_modem(false)
elseif wl_nic and wl_nic.is_modem(device) then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
databus.tx_hw_wl_modem(false)
end
-- we only care if this is our active comms modem
if _bp.act_nic.is_modem(device) then
print_no_fp("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.act_nic == wl_nic then
-- wireless active disconnected
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if wl_nic and modem then
log.info("BKPLN: found another wireless modem, using it for comms")
wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
databus.tx_hw_wl_modem(true)
elseif wd_nic and wd_nic.is_connected() then
_bp.act_nic = wd_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wired modem")
end
elseif wl_nic and wl_nic.is_connected() then
-- wired active disconnected, wireless available
_bp.act_nic = wl_nic
comms.switch_nic(_bp.act_nic, _bp.smem.rtu_state)
log.info("BKPLN: switched comms to wireless modem")
else
-- wired active disconnected, wireless unavailable
end
elseif wd_nic and wd_nic.is_modem(device) then
-- wired, but not active
print_no_fp("standby wired modem disconnected")
log.info("BKPLN: standby wired modem disconnected")
elseif wl_nic and wl_nic.is_modem(device) then
-- wireless, but not active
print_no_fp("standby wireless modem disconnected")
log.info("BKPLN: standby wireless modem disconnected")
else
print_no_fp("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
elseif type == "speaker" then
---@cast device Speaker
log.info("BKPLN: SPEAKER LINK_DOWN " .. iface)
for i = 1, #_bp.sounders do
if _bp.sounders[i].speaker == device then
table.remove(_bp.sounders, i)
print_no_fp("a speaker was disconnected")
log.warning("BKPLN: speaker sounder " .. iface .. " disconnected")
databus.tx_hw_spkr_count(#_bp.sounders)
break
end
end
end
end
return backplane

View File

@@ -27,13 +27,17 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
checking_wl = true,
wd_modem = nil, ---@type Modem|nil
wl_modem = nil, ---@type Modem|nil
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
self_check_pass = true,
self_check_wireless = true,
settings = nil, ---@type rtu_config
run_test_btn = nil, ---@type PushButton
@@ -59,10 +63,9 @@ local function send_sv(msg_type, msg)
local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
s_pkt.make(comms.BROADCAST, util.time_ms() * 10, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.settings.SVR_Channel, self.settings.RTU_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
@@ -78,10 +81,7 @@ local function handle_packet(packet)
local est_ack = packet.data[1]
if est_ack== ESTABLISH_ACK.ALLOW then
self.self_check_msg(nil, true, "")
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
if self.self_check_pass then check_complete() end
-- OK
elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied"
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
@@ -98,18 +98,20 @@ local function handle_packet(packet)
end
self.net_listen = false
self.run_test_btn.enable()
if error_msg then
self.self_check_msg(nil, false, error_msg)
else
self.self_check_msg(nil, true, "")
end
util.push_event("conn_test_complete", error_msg == nil)
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.run_test_btn.enable()
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
util.push_event("conn_test_complete", false)
end
@@ -129,10 +131,18 @@ local function self_check()
self.self_check_pass = true
local cfg = self.settings
local modem = ppm.get_wireless_modem()
self.wd_modem = ppm.get_modem(cfg.WiredModem)
self.wl_modem = ppm.get_wireless_modem()
local valid_cfg = rtu.validate_config(cfg)
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway")
if cfg.WiredModem then
self.self_check_msg("> check wired comms modem connected...", self.wd_modem, "please connect the wired comms modem " .. cfg.WiredModem)
end
if cfg.WirelessModem then
self.self_check_msg("> check wireless/ender modem connected...", self.wl_modem, "please connect an ender or wireless modem for wireless comms")
end
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
@@ -211,8 +221,11 @@ local function self_check()
end
end
if valid_cfg and modem then
self.self_check_msg("> check supervisor connection...")
if valid_cfg then
self.checking_wl = true
if cfg.WirelessModem and self.wl_modem then
self.self_check_msg("> check wireless supervisor connection...")
-- init mac as needed
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
@@ -221,17 +234,24 @@ local function self_check()
network.deinit_mac()
end
self.nic = network.nic(modem)
comms.set_trusted_range(cfg.TrustedRange)
self.nic = network.nic(self.wl_modem)
self.nic.closeAll()
self.nic.open(cfg.RTU_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.RTU, {} })
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.RTU, {} })
tcd.dispatch_unique(8, handle_timeout)
elseif cfg.WiredModem and self.wd_modem then
-- skip to wired
util.push_event("conn_test_complete", true)
else
self.self_check_msg("> no modem, can't test supervisor connection", false)
end
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
@@ -315,4 +335,44 @@ function check.receive_sv(side, sender, reply_to, message, distance)
end
end
-- handle completed connection tests
---@param pass boolean
function check.conn_test_callback(pass)
local cfg = self.settings
if self.checking_wl then
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wireless interface, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
end
if cfg.WiredModem and self.wd_modem then
self.checking_wl = false
self.self_check_msg("> check wired supervisor connection...")
comms.set_trusted_range(0)
self.nic = network.nic(self.wd_modem)
self.nic.closeAll()
self.nic.open(cfg.RTU_Channel)
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, comms.CONN_TEST_FWV, DEVICE_TYPE.RTU, {} })
tcd.dispatch_unique(8, handle_timeout)
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
else
if not pass then
self.self_check_msg(nil, false, "make sure your supervisor is running, listening on the wired interface, the wire is intact, and your channels are correct")
end
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
end
return check

View File

@@ -484,7 +484,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
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}
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),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}

View File

@@ -30,6 +30,10 @@ local self = {
importing_legacy = false,
importing_any_dc = false,
wireless = nil, ---@type Checkbox
wl_pref = nil, ---@type Checkbox
wired = nil, ---@type Checkbox
range = nil, ---@type NumberField
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
@@ -90,22 +94,88 @@ 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 function en_dis_pref()
if self.wireless.get_value() and self.wired.get_value() then
self.wl_pref.enable()
else
self.wl_pref.set_value(self.wireless.get_value())
self.wl_pref.disable()
end
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 on_wired_change(_)
en_dis_pref()
tool_ctl.gen_modem_list()
end
self.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=en_dis_pref}
self.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}
self.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="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",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}
en_dis_pref()
local function submit_interfaces()
tmp_cfg.WirelessModem = self.wireless.get_value()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PreferWireless = self.wl_pref.get_value()
else
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem
self.wl_pref.set_value(tmp_cfg.PreferWireless)
end
if not self.wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (self.wired.get_value() or self.wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif self.wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
if tmp_cfg.WirelessModem then
self.range.enable()
else
self.range.set_value(0)
self.range.disable()
end
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 +183,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,54 +194,62 @@ 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)"}
self.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,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents wireless 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 n3_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())
local range_val = tonumber(range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = range_val
net_pane.set_value(3)
p2_err.hide(true)
elseif timeout_val == nil then
p2_err.set_value("Please set the connection timeout.")
p2_err.show()
local range_val = tonumber(self.range.get_value())
if timeout_val == nil then
n3_err.set_value("Please set the connection timeout.")
n3_err.show()
elseif tmp_cfg.WirelessModem and (range_val == nil) then
n3_err.set_value("Please set the trusted range.")
n3_err.show()
else
p2_err.set_value("Please set the trusted range.")
p2_err.show()
tmp_cfg.ConnTimeout = timeout_val
tmp_cfg.TrustedRange = tri(tmp_cfg.WirelessModem, range_val, 0)
if tmp_cfg.WirelessModem then
net_pane.set_value(4)
else
main_pane.set_value(4)
tmp_cfg.AuthKey = ""
end
n3_err.hide(true)
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 for 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 +260,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
@@ -196,7 +274,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -238,7 +316,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
@@ -382,10 +460,13 @@ 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(self.wireless, ini_cfg.WirelessModem)
try_set(self.wired, ini_cfg.WiredModem ~= false)
try_set(self.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)
try_set(range, ini_cfg.TrustedRange)
try_set(self.range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
@@ -665,6 +746,59 @@ 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()
modem_list.remove_all()
local enable = self.wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
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 end
if 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
@@ -87,11 +90,14 @@ local tmp_cfg = {
SpeakerVolume = 1.0,
Peripherals = {}, ---@type rtu_peri_definition[]
Redstone = {}, ---@type rtu_rs_definition[]
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
SVR_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil
AuthKey = nil, ---@type string
LogMode = 0, ---@type LOG_MODE
LogPath = "",
LogDebug = false,
@@ -106,6 +112,9 @@ local settings_cfg = {}
local fields = {
{ "SpeakerVolume", "Speaker Volume", 1.0 },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PreferWireless", "Prefer Wireless Modem", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "RTU_Channel", "RTU Channel", 16242 },
{ "ConnTimeout", "Connection Timeout", 5 },
@@ -313,6 +322,9 @@ 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.WiredModem = ini_cfg.WiredModem
tmp_cfg.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
@@ -329,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()
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -345,16 +359,20 @@ function configurator.configure(ask_config)
display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
elseif event == "conn_test_complete" then
check.conn_test_callback(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
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

@@ -23,7 +23,7 @@ databus.RTU_HW_STATE = RTU_HW_STATE
-- call to toggle heartbeat signal
function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
-- transmit firmware versions
---@param rtu_v string RTU version
---@param comms_v string comms version
function databus.tx_versions(rtu_v, comms_v)
@@ -31,10 +31,16 @@ function databus.tx_versions(rtu_v, comms_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit hardware status for modem connection state
-- transmit hardware status for the wired comms modem
---@param has_modem boolean
function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem)
function databus.tx_hw_wd_modem(has_modem)
databus.ps.publish("has_wd_modem", has_modem)
end
-- transmit hardware status for the wireless comms modem
---@param has_modem boolean
function databus.tx_hw_wl_modem(has_modem)
databus.ps.publish("has_wl_modem", has_modem)
end
-- transmit the number of speakers connected
@@ -43,14 +49,14 @@ function databus.tx_hw_spkr_count(count)
databus.ps.publish("speaker_count", count)
end
-- transmit unit hardware type across the bus
-- transmit unit hardware type
---@param uid integer unit ID
---@param type RTU_UNIT_TYPE
function databus.tx_unit_hw_type(uid, type)
databus.ps.publish("unit_type_" .. uid, type)
end
-- transmit unit hardware status across the bus
-- transmit unit hardware status
---@param uid integer unit ID
---@param status RTU_HW_STATE
function databus.tx_unit_hw_status(uid, status)
@@ -64,17 +70,10 @@ function databus.tx_rt_status(thread, ok)
databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
-- transmit supervisor link state
---@param state integer
function databus.tx_link_state(state)
databus.ps.publish("link_state", state)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

View File

@@ -12,6 +12,7 @@ local style = require("rtu.panel.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
@@ -25,6 +26,7 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local ind_grn = style.ind_grn
@@ -32,8 +34,11 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
-- create new front panel view
---@param panel DisplayBox main displaybox
---@param config rtu_config configuraiton
---@param units rtu_registry_entry[] unit list
local function init(panel, units)
local function init(panel, config, units)
local s_hi_box = style.theme.highlight_box
local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
@@ -53,7 +58,15 @@ local function init(panel, units)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=ind_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=ind_grn}
wd_modem.register(databus.ps, "has_wd_modem", wd_modem.update)
wl_modem.register(databus.ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
modem.register(databus.ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
@@ -90,8 +103,6 @@ local function init(panel, units)
system.line_break()
modem.register(databus.ps, "has_modem", modem.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn}
local rt_comm = LED{parent=system,label="RT COMMS",colors=ind_grn}
system.line_break()
@@ -99,25 +110,27 @@ local function init(panel, units)
rt_main.register(databus.ps, "routine__main", rt_main.update)
rt_comm.register(databus.ps, "routine__comms", rt_comm.update)
--
-- hardware labeling
--
local hw_labels = Rectangle{parent=panel,x=2,y=term_h-6,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
local comp_id = util.sprintf("%03d", os.getComputerID())
TextBox{parent=system,y=term_h-5,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=term_h-5,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
TextBox{parent=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-RTU",fg_bg=s_hi_box}
--
-- speaker count
--
TextBox{parent=panel,x=2,y=term_h-1,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=panel,x=11,y=term_h-1,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
--
-- about label
--
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- unit status list
--

View File

@@ -18,16 +18,15 @@ local ui = {
}
-- try to start the UI
---@param config rtu_config configuration
---@param units rtu_registry_entry[] RTU entries
---@param theme FP_THEME front panel theme
---@param color_mode COLOR_MODE color mode
---@return boolean success, any error_msg
function renderer.try_start_ui(units, theme, color_mode)
function renderer.try_start_ui(config, units)
local status, msg = true, nil
if ui.display == nil then
-- set theme
style.set_theme(theme, color_mode)
style.set_theme(config.FrontPanelTheme, config.ColorMode)
-- reset terminal
term.setTextColor(colors.white)
@@ -41,7 +40,7 @@ function renderer.try_start_ui(units, theme, color_mode)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[color_mode]
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
@@ -49,7 +48,7 @@ function renderer.try_start_ui(units, theme, color_mode)
-- init front panel view
status, msg = pcall(function ()
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(ui.display, units)
panel_view(ui.display, config, units)
end)
if status then

View File

@@ -33,6 +33,9 @@ function rtu.load_config()
config.SpeakerVolume = settings.get("SpeakerVolume")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.SVR_Channel = settings.get("SVR_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
config.ConnTimeout = settings.get("ConnTimeout")
@@ -57,6 +60,10 @@ function rtu.validate_config(cfg)
cfv.assert_type_num(cfg.SpeakerVolume)
cfv.assert_range(cfg.SpeakerVolume, 0, 3)
cfv.assert_type_bool(cfg.WirelessModem)
cfv.assert((cfg.WiredModem == false) or (type(cfg.WiredModem) == "string"))
cfv.assert(cfg.WirelessModem or (type(cfg.WiredModem) == "string"))
cfv.assert_type_bool(cfg.PreferWireless)
cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.RTU_Channel)
cfv.assert_type_num(cfg.ConnTimeout)
@@ -299,13 +306,11 @@ function rtu.comms(version, nic, conn_watchdog)
local insert = table.insert
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
end
-- PRIVATE FUNCTIONS --
-- configure modem channels
nic.closeAll()
nic.open(config.RTU_Channel)
--#region PRIVATE FUNCTIONS --
-- send a scada management packet
---@param msg_type MGMT_TYPE
@@ -345,18 +350,19 @@ function rtu.comms(version, nic, conn_watchdog)
return advertisement
end
-- PUBLIC FUNCTIONS --
--#endregion
--#region PUBLIC FUNCTIONS --
---@class rtu_comms
local public = {}
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1
-- switch the current active NIC
---@param act_nic nic
---@param rtu_state rtu_state
function public.switch_nic(act_nic, rtu_state)
public.close(rtu_state)
nic = act_nic
end
-- unlink from the server
@@ -376,6 +382,17 @@ function rtu.comms(version, nic, conn_watchdog)
_send(MGMT_TYPE.CLOSE, {})
end
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1
end
-- send establish request (includes advertisement)
---@param units table
function public.send_establish(units)
@@ -594,6 +611,8 @@ function rtu.comms(version, nic, conn_watchdog)
end
end
--#endregion
return public
end

View File

@@ -1,5 +1,5 @@
--
-- RTU: Remote Terminal Unit
-- RTU Gateway: Remote Terminal Unit Gateway
--
require("/initenv").init_env()
@@ -11,30 +11,17 @@ local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local backplane = require("rtu.backplane")
local configure = require("rtu.configure")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local uinit = require("rtu.uinit")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu")
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.3"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
local RTU_VERSION = "v1.13.0"
local println = util.println
local println_ts = util.println_ts
@@ -82,7 +69,7 @@ local function main()
-- startup
----------------------------------------
-- record firmware versions and ID
-- report versions
databus.tx_versions(RTU_VERSION, comms.version)
-- mount connected devices
@@ -106,15 +93,9 @@ local function main()
shutdown = false
},
-- RTU gateway devices (not RTU units)
rtu_dev = {
modem = ppm.get_wireless_modem(),
sounders = {} ---@type rtu_speaker_sounder[]
},
-- system objects
---@class rtu_sys
rtu_sys = {
nic = nil, ---@type nic
rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog
units = {} ---@type rtu_registry_entry[]
@@ -127,467 +108,22 @@ local function main()
}
local smem_sys = __shared_memory.rtu_sys
local smem_dev = __shared_memory.rtu_dev
local rtu_state = __shared_memory.rtu_state
----------------------------------------
-- interpret config and init units
----------------------------------------
local units = __shared_memory.rtu_sys.units
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()
--#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 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
elseif entry.unit == nil then
assignment = "facility"
for_reactor = 0
else
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message)
log.fatal(message)
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 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)
println(message)
log.fatal(message)
return false
else
-- link redstone in RTU
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(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
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
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
table.insert(bank, entry)
end
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.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(conns, entry.port)
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 _, 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,
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,
thread = nil
}
table.insert(units, unit)
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
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
--#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name
local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string
if type(name) ~= "string" then
local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
println(message)
log.fatal(message)
return false
end
-- CHECK: index type
if (index ~= nil) and (not util.is_int(index)) then
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
println(message)
log.fatal(message)
return false
end
-- CHECK: index range
local function validate_index(min, max)
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
if max ~= nil then message = util.c(message, " and <= ", max) end
println(message)
log.fatal(message)
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("sys_config> 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("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
println(message)
log.fatal(message)
return false
else return true end
end
local device = ppm.get_periph(name)
local type ---@type string|nil
local rtu_iface ---@type rtu_device
local rtu_type ---@type RTU_UNIT_TYPE
local is_multiblock = false ---@type boolean
local formed = nil ---@type boolean|nil
local faulted = nil ---@type boolean|nil
if device == nil then
local message = util.c("sys_config> '", name, "' not found, using placeholder")
println(message)
log.warning(message)
-- mount a virtual (placeholder) device
type, device = ppm.mount_virtual()
else
type = ppm.get_type(name)
end
if type == "boilerValve" then
-- 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_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
end
elseif type == "turbineValve" then
-- 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_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
end
elseif type == "dynamicValve" then
-- 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_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
end
elseif type == "inductionPort" or type == "reinforcedInductionPort" then
-- induction matrix multiblock (normal or reinforced)
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
end
elseif type == "spsPort" then
-- SPS multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
end
elseif type == "solarNeutronActivator" then
-- SNA
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" or type == "environment_detector" then
-- advanced peripherals environment detector
if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device
rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface()
else
local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
println_ts(message)
log.fatal(message)
return false
end
if is_multiblock then
if not formed then
if formed == false then
log.info(util.c("sys_config> device '", name, "' is not formed"))
else formed = false end
elseif faulted then
-- sometimes there is a race condition on server boot where it reports formed, but
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
formed = false
log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end
end
---@class rtu_registry_entry
local rtu_unit = {
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)
table.insert(units, rtu_unit)
local for_message = "the facility"
if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor)
end
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
rtu_unit.uid = #units
-- determine hardware status
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
rtu_unit.hw_state = RTU_HW_STATE.OFFLINE
else
if rtu_unit.is_multiblock then
rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED)
elseif faulted then
rtu_unit.hw_state = RTU_HW_STATE.FAULTED
else
rtu_unit.hw_state = RTU_HW_STATE.OK
end
end
-- report hardware status
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end
--#endregion
return true
end
----------------------------------------
-- start system
-- init and start system
----------------------------------------
log.debug("boot> running sys_config()")
-- modem and speaker initialization
if not backplane.init(config, __shared_memory) then return end
if sys_config() then
-- check modem
if smem_dev.modem == nil then
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- find and setup all speakers
local speakers = ppm.get_all_devices("speaker")
for _, s in pairs(speakers) do
local sounder = rtu.init_sounder(s)
table.insert(smem_dev.sounders, sounder)
log.debug(util.c("startup> added speaker, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#smem_dev.sounders)
log.debug("startup> running uinit()")
if uinit(config, __shared_memory) then
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
rtu_state.fp_ok, message = renderer.try_start_ui(config, units)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
@@ -601,8 +137,7 @@ local function main()
log.debug("startup> conn watchdog started")
-- setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, backplane.active_nic(), smem_sys.conn_watchdog)
log.debug("startup> comms init")
-- init threads

View File

@@ -5,10 +5,10 @@ local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local backplane = require("rtu.backplane")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
@@ -25,8 +25,8 @@ local threads = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local COMMS_SLEEP = 100 -- 100ms, 2 ticks
---@param smem rtu_shared_memory
---@param println_ts function
@@ -184,19 +184,19 @@ function threads.thread__main(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("main", true)
log.debug("main thread start")
log.debug("OS: main thread start")
-- main loop clock
local loop_clock = util.new_clock(MAIN_CLOCK)
-- load in from shared memory
local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local nic = smem.rtu_sys.nic
local rtu_comms = smem.rtu_sys.rtu_comms
local conn_watchdog = smem.rtu_sys.conn_watchdog
local units = smem.rtu_sys.units
local sounders = backplane.sounders()
-- start unlinked (in case of restart)
rtu_comms.unlink(rtu_state)
@@ -246,38 +246,8 @@ function threads.thread__main(smem)
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only care if this is our wireless modem
if nic.is_modem(device) then
nic.disconnect()
println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
databus.tx_hw_modem(false)
end
else
log.warning("non-comms modem disconnected")
end
elseif type == "speaker" then
---@cast device Speaker
for i = 1, #sounders do
if sounders[i].speaker == device then
table.remove(sounders, i)
log.warning(util.c("speaker ", param1, " disconnected"))
println_ts("speaker disconnected")
databus.tx_hw_spkr_count(#sounders)
break
end
end
if type == "modem" or type == "speaker" then
backplane.detach(type, device, param1, println_ts)
else
for i = 1, #units do
-- find disconnected device
@@ -301,29 +271,8 @@ function threads.thread__main(smem)
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
nic.connect(device)
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
databus.tx_hw_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log.info("wired modem reconnected")
end
elseif type == "speaker" then
---@cast device Speaker
table.insert(sounders, rtu.init_sounder(device))
println_ts("speaker connected")
log.info(util.c("connected speaker ", param1))
databus.tx_hw_spkr_count(#sounders)
if type == "modem" or type == "speaker" then
backplane.attach(type, device, param1, println_ts)
else
-- relink lost peripheral to correct unit entry
for i = 1, #units do
@@ -349,7 +298,7 @@ function threads.thread__main(smem)
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
rtu_state.shutdown = true
log.info("terminate requested, main thread exiting")
log.info("OS: terminate requested, main thread exiting")
break
end
end
@@ -368,7 +317,7 @@ function threads.thread__main(smem)
databus.tx_rt_status("main", false)
if not rtu_state.shutdown then
log.info("main thread restarting in 5 seconds...")
log.info("OS: main thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -387,16 +336,16 @@ function threads.thread__comms(smem)
-- execute thread
function public.exec()
databus.tx_rt_status("comms", true)
log.debug("comms thread start")
log.debug("OS: comms thread start")
-- load in from shared memory
local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local rtu_comms = smem.rtu_sys.rtu_comms
local units = smem.rtu_sys.units
local comms_queue = smem.q.mq_comms
local sounders = backplane.sounders()
local last_update = util.time()
-- thread loop
@@ -421,7 +370,7 @@ function threads.thread__comms(smem)
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning("comms thread exceeded 100ms queue process limit")
log.warning("OS: comms thread exceeded 100ms queue process limit")
break
end
end
@@ -432,7 +381,7 @@ function threads.thread__comms(smem)
-- check for termination request
if rtu_state.shutdown then
rtu_comms.close(rtu_state)
log.info("comms thread exiting")
log.info("OS: comms thread exiting")
break
end
@@ -454,7 +403,7 @@ function threads.thread__comms(smem)
databus.tx_rt_status("comms", false)
if not rtu_state.shutdown then
log.info("comms thread restarting in 5 seconds...")
log.info("OS: comms thread restarting in 5 seconds...")
util.psleep(5)
end
end
@@ -477,7 +426,7 @@ function threads.thread__unit_comms(smem, unit)
-- execute thread
function public.exec()
databus.tx_rt_status("unit_" .. unit.uid, true)
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
log.debug(util.c("OS: rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")"))
-- load in from shared memory
local rtu_state = smem.rtu_state
@@ -494,7 +443,7 @@ function threads.thread__unit_comms(smem, unit)
local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
if packet_queue == nil then
log.error("rtu unit thread created without a message queue, exiting...", true)
log.error("OS: rtu unit thread created without a message queue, exiting...", true)
return
end
@@ -522,7 +471,7 @@ function threads.thread__unit_comms(smem, unit)
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. short_name)
log.info("OS: rtu unit thread exiting -> " .. short_name)
break
end
@@ -587,7 +536,7 @@ function threads.thread__unit_comms(smem, unit)
databus.tx_rt_status("unit_" .. unit.uid, false)
if not rtu_state.shutdown then
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
log.info(util.c("OS: rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
util.psleep(5)
end
end

441
rtu/uinit.lua Normal file
View File

@@ -0,0 +1,441 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local rtu = require("rtu.rtu")
local threads = require("rtu.threads")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local redstone_rtu = require("rtu.dev.redstone_rtu")
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 println = util.println
local println_ts = util.println_ts
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE
-- print and log a fatal error during startup
---@param msg string
local function log_fail(msg)
println(msg)
log.fatal(msg)
end
-- 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
---@param config rtu_config
---@param __shared_memory rtu_shared_memory
---@return boolean success
return function(config, __shared_memory)
local units = __shared_memory.rtu_sys.units
local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals
--#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 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
elseif entry.unit == nil then
assignment = "facility"
for_reactor = 0
else
log_fail(util.c("uinit> invalid unit assignment at block index #", entry_idx))
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
log_fail(util.c("uinit> invalid redstone relay '", entry.relay, '"'))
return false
elseif not rs_rtus[entry.relay] then
log.debug(util.c("uinit> 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("uinit> 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("uinit> 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("uinit> 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 bank = rs_rtus[phy].banks[for_reactor]
local conns = all_conns[for_reactor]
if not valid then
log_fail(util.c("uinit> invalid redstone definition at block index #", entry_idx))
return false
else
-- link redstone in RTU
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(conns, entry.port) then
local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
table.insert(bank, entry)
end
elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs
if util.table_contains(conns, entry.port) then
local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message)
log.warning(message)
else
table.insert(bank, entry)
end
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.fatal("uinit> failed to identify IO mode at block index #" .. entry_idx)
println("uinit> encountered a software error, check logs")
return false
end
table.insert(conns, entry.port)
log.debug(util.c("uinit> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
end
end
-- create unit entries for redstone RTUs
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("uinit> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
println("uinit> encountered a software error, check logs")
return false
end
table.insert(conns, conn.port)
log.debug(util.c("uinit> 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,
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,
thread = nil
}
table.insert(units, unit)
local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
log.info(util.c("uinit> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end
--#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name
local index = entry.index
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
-- CHECK: name is a string
if type(name) ~= "string" then
log_fail(util.c("uinit> device entry #", i, ": device ", name, " isn't a string"))
return false
end
-- CHECK: index type
if (index ~= nil) and (not util.is_int(index)) then
log_fail(util.c("uinit> device entry #", i, ": index ", index, " isn't valid"))
return false
end
-- CHECK: index range
local function validate_index(min, max)
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
local message = util.c("uinit> device entry #", i, ": index ", index, " isn't >= ", min)
if max ~= nil then message = util.c(message, " and <= ", max) end
log_fail(message)
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
log_fail(util.c("uinit> device entry #", i, ": must only be for the facility"))
return false
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
log_fail(util.c("uinit> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild"))
return false
else return true end
end
local device = ppm.get_periph(name)
local type ---@type string|nil
local rtu_iface ---@type rtu_device
local rtu_type ---@type RTU_UNIT_TYPE
local is_multiblock = false ---@type boolean
local formed = nil ---@type boolean|nil
local faulted = nil ---@type boolean|nil
if device == nil then
local message = util.c("uinit> '", name, "' not found, using placeholder")
println(message)
log.warning(message)
-- mount a virtual (placeholder) device
type, device = ppm.mount_virtual()
else
type = ppm.get_type(name)
end
if type == "boilerValve" then
-- 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_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed boiler multiblock"))
end
elseif type == "turbineValve" then
-- 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_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed turbine multiblock"))
end
elseif type == "dynamicValve" then
-- 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_iface, faulted = dynamicv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed dynamic tank multiblock"))
end
elseif type == "inductionPort" or type == "reinforcedInductionPort" then
-- induction matrix multiblock (normal or reinforced)
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed induction matrix multiblock"))
end
elseif type == "spsPort" then
-- SPS multiblock
if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
if formed == ppm.ACCESS_FAULT then
println_ts(util.c("uinit> failed to check if '", name, "' is formed"))
log.warning(util.c("uinit> failed to check if '", name, "' is a formed SPS multiblock"))
end
elseif type == "solarNeutronActivator" then
-- SNA
if not validate_assign() then return false end
rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" or type == "environment_detector" then
-- advanced peripherals environment detector
if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface, faulted = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device
rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface()
else
log_fail(util.c("uinit> device '", name, "' is not a known type (", type, ")"))
return false
end
if is_multiblock then
if not formed then
if formed == false then
log.info(util.c("uinit> device '", name, "' is not formed"))
else formed = false end
elseif faulted then
-- sometimes there is a race condition on server boot where it reports formed, but
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
formed = false
log.warning(util.c("uinit> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end
end
---@class rtu_registry_entry
local rtu_unit = {
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)
table.insert(units, rtu_unit)
local for_message = "the facility"
if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor)
end
local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "")
log.info(util.c("uinit> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message))
rtu_unit.uid = #units
-- determine hardware status
if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then
rtu_unit.hw_state = RTU_HW_STATE.OFFLINE
else
if rtu_unit.is_multiblock then
rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED)
elseif faulted then
rtu_unit.hw_state = RTU_HW_STATE.FAULTED
else
rtu_unit.hw_state = RTU_HW_STATE.OK
end
end
-- report hardware status
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end
--#endregion
return true
end

View File

@@ -17,7 +17,7 @@ local max_distance = nil
local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "3.0.8"
comms.version = "3.1.0"
comms.api_version = "0.0.10"
---@enum PROTOCOL
@@ -49,13 +49,14 @@ local MGMT_TYPE = {
ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection
RTU_ADVERT = 3, -- RTU capability advertisement
RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones
DIAG_TONE_GET = 6, -- (API) diagnostic: get alarm tones
DIAG_TONE_SET = 7, -- (API) diagnostic: set alarm tones
DIAG_ALARM_SET = 8, -- (API) diagnostic: set alarm to simulate audio for
INFO_LIST_CMP = 9 -- (API) info: list all computers on the network
PROBE = 3,
RTU_ADVERT = 4, -- RTU capability advertisement
RTU_DEV_REMOUNT = 5, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
RTU_TONE_ALARM = 6, -- instruct RTUs to play specified alarm tones
DIAG_TONE_GET = 7, -- (API) diagnostic: get alarm tones
DIAG_TONE_SET = 8, -- (API) diagnostic: set alarm tones
DIAG_ALARM_SET = 9, -- (API) diagnostic: set alarm to simulate audio for
INFO_LIST_CMP = 10 -- (API) info: list all computers on the network
}
---@enum CRDN_TYPE
@@ -89,6 +90,12 @@ local ESTABLISH_ACK = {
---@enum DEVICE_TYPE device types for establish messages
local DEVICE_TYPE = { PLC = 0, RTU = 1, SVR = 2, CRD = 3, PKT = 4 }
---@enum PROBE_ACK
local PROBE_ACK = {
OPEN = 0,
CONFLICT = 1
}
---@enum PLC_AUTO_ACK
local PLC_AUTO_ACK = {
FAIL = 0, -- failed to set burn rate/burn rate invalid
@@ -130,6 +137,8 @@ comms.CRDN_TYPE = CRDN_TYPE
comms.ESTABLISH_ACK = ESTABLISH_ACK
comms.DEVICE_TYPE = DEVICE_TYPE
comms.PROBE_ACK = PROBE_ACK
comms.PLC_AUTO_ACK = PLC_AUTO_ACK
comms.UNIT_COMMAND = UNIT_COMMAND
@@ -138,6 +147,9 @@ comms.FAC_COMMAND = FAC_COMMAND
-- destination broadcast address (to all devices)
comms.BROADCAST = -1
-- firmware version used to indicate an establish packet is a connection test
comms.CONN_TEST_FWV = "CONN_TEST"
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame
@@ -205,7 +217,7 @@ function comms.scada_packet()
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance
-- log.debug("COMMS: comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
-- log.debug("COMMS: scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else
if type(self.raw) == "table" then
if #self.raw == 5 then
@@ -251,6 +263,8 @@ function comms.scada_packet()
---@nodiscard
function public.raw_sendable() return self.raw end
---@nodiscard
function public.interface() return self.modem_msg_in.iface end
---@nodiscard
function public.local_channel() return self.modem_msg_in.s_channel end
---@nodiscard
@@ -326,7 +340,7 @@ function comms.authd_packet()
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
-- outside of maximum allowable transmission distance
-- log.debug("COMMS: comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
-- log.debug("COMMS: authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else
if type(self.raw) == "table" then
if #self.raw == 4 then

View File

@@ -49,7 +49,7 @@ function network.init_mac(passkey)
_crypt.hmac.setKey(_crypt.key)
local init_time = util.time_ms() - start
log.info("NET: network.init_mac completed in " .. init_time .. "ms")
log.info("NET: network.init_mac() completed in " .. init_time .. "ms")
return init_time
end
@@ -190,13 +190,13 @@ function network.nic(modem)
---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac)
-- log.debug("NET: network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
-- log.debug("NET: network.modem.transmit(): data processing took " .. (util.time_ms() - start) .. "ms")
end
---@diagnostic disable-next-line: need-check-nil
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
else
log.debug("NET: network.transmit tx dropped, link is down")
log.debug("NET: network.transmit() tx dropped, link is down")
end
end
@@ -227,10 +227,10 @@ function network.nic(modem)
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then
-- log.debug("NET: network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
-- log.debug("NET: network.modem.receive(): HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.stamp_authenticated()
else
-- log.debug("NET: network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
-- log.debug("NET: network.modem.receive(): HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
end
end

View File

@@ -293,7 +293,7 @@ function ppm.remount(iface)
return pm_type, pm_dev
end
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
-- mount a virtual placeholder device
---@nodiscard
---@return string type, table device
function ppm.mount_virtual()
@@ -409,13 +409,13 @@ end
-- get all mounted peripherals by type
---@nodiscard
---@param name string type name
---@param type string type name
---@return table devices device function tables
function ppm.get_all_devices(name)
function ppm.get_all_devices(type)
local devices = {}
for _, data in pairs(_ppm.mounts) do
if data.type == name then
if data.type == type then
table.insert(devices, data.dev)
end
end
@@ -447,22 +447,49 @@ end
---@return table|nil reactor function table
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
-- get a modem by name
---@nodiscard
---@param iface string CC peripheral interface
---@return Modem|nil modem function table
function ppm.get_modem(iface)
local modem = nil
local device = _ppm.mounts[iface]
if device and device.type == "modem" then modem = device.dev end
return modem
end
-- get the wireless modem (if multiple, returns the first)<br>
-- if this is in a CraftOS emulated environment, wired modems will be used instead
---@nodiscard
---@return Modem|nil modem function table
---@return Modem|nil modem, string|nil iface
function ppm.get_wireless_modem()
local w_modem = nil
local w_modem, w_iface = nil, nil
local emulated_env = periphemu ~= nil
for _, device in pairs(_ppm.mounts) do
for iface, device in pairs(_ppm.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
w_iface = iface
w_modem = device.dev
break
end
end
return w_modem
return w_modem, w_iface
end
-- list all connected wired modems
---@nodiscard
---@return { [string]: ppm_entry } modems
function ppm.get_wired_modem_list()
local list = {}
for iface, device in pairs(_ppm.mounts) do
if device.type == "modem" and not device.dev.isWireless() then list[iface] = device end
end
return list
end
-- list all connected monitors

View File

@@ -212,6 +212,13 @@ end
--#region ENUMERATION TYPES
---@enum LISTEN_MODE
types.LISTEN_MODE = {
WIRELESS = 1,
WIRED = 2,
ALL = 3
}
---@enum TEMP_SCALE
types.TEMP_SCALE = {
KELVIN = 1,
@@ -561,7 +568,7 @@ types.ALARM_STATE_NAMES = {
---| "websocket_failure"
---| "websocket_message"
---| "websocket_success"
---| "clock_start" (custom)
---| "conn_test_complete" (custom)
---@alias fluid
---| "mekanism:empty_gas"

View File

@@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
util.version = "1.5.5"
util.version = "1.6.0"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50

171
supervisor/backplane.lua Normal file
View File

@@ -0,0 +1,171 @@
--
-- Supervisor System Core Peripheral Backplane
--
local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local databus = require("supervisor.databus")
local println = util.println
---@class supervisor_backplane
local backplane = {}
local _bp = {
config = nil, ---@type svr_config
lan_iface = false, ---@type string|false wired comms modem name
wd_nic = nil, ---@type nic|nil wired nic
wl_nic = nil, ---@type nic|nil wireless nic
nic_map = {} ---@type nic[] connected nics
}
-- network interfaces indexed by peripheral names
backplane.nics = _bp.nic_map
-- initialize the system peripheral backplane
---@param config svr_config
---@return boolean success
function backplane.init(config)
_bp.lan_iface = config.WiredModem
-- setup the wired modem, if configured
if type(_bp.lan_iface) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
local wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
if not (modem and _bp.lan_iface) then
println("startup> wired comms modem not found")
log.fatal("BKPLN: no wired comms modem on startup")
return false
end
_bp.wd_nic = wd_nic
_bp.nic_map[_bp.lan_iface] = wd_nic
wd_nic.closeAll()
wd_nic.open(config.SVR_Channel)
databus.tx_hw_wd_modem(true)
end
-- setup the wireless modem, if configured
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
local wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN") .. (iface or ""))
if not (modem and iface) then
println("startup> wireless comms modem not found")
log.fatal("BKPLN: no wireless comms modem on startup")
return false
end
_bp.wl_nic = wl_nic
_bp.nic_map[iface] = wl_nic
wl_nic.closeAll()
wl_nic.open(config.SVR_Channel)
databus.tx_hw_wl_modem(true)
end
return true
end
-- handle a backplane peripheral attach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.attach(iface, type, device, print_no_fp)
if type == "modem" then
---@cast device Modem
local m_is_wl = device.isWireless()
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_ATTACH ", iface))
if _bp.wd_nic and (_bp.lan_iface == iface) then
-- connect this as the wired NIC
_bp.wd_nic.connect(device)
log.info("BKPLN: WIRED PHY_UP " .. iface)
print_no_fp("wired comms modem reconnected")
databus.tx_hw_wd_modem(true)
elseif _bp.wl_nic and (not _bp.wl_nic.is_connected()) and m_is_wl then
-- connect this as the wireless NIC
_bp.wl_nic.connect(device)
_bp.nic_map[iface] = _bp.wl_nic
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem reconnected")
databus.tx_hw_wl_modem(true)
elseif _bp.wl_nic and m_is_wl then
-- the wireless NIC already has a modem
device.closeAll()
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
device.closeAll()
print_no_fp("unassigned modem connected")
log.warning("BKPLN: unassigned modem connected")
end
end
end
-- handle a backplane peripheral detach
---@param iface string
---@param type string
---@param device table
---@param print_no_fp function
function backplane.detach(iface, type, device, print_no_fp)
if type == "modem" then
---@cast device Modem
log.info(util.c("BKPLN: PHY_DETACH ", iface))
_bp.nic_map[iface] = nil
if _bp.wd_nic and _bp.wd_nic.is_modem(device) then
_bp.wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
print_no_fp("wired modem disconnected")
log.warning("BKPLN: wired comms modem disconnected")
databus.tx_hw_wd_modem(false)
elseif _bp.wl_nic and _bp.wl_nic.is_modem(device) then
_bp.wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
print_no_fp("wireless comms modem disconnected")
log.warning("BKPLN: wireless comms modem disconnected")
local modem, m_iface = ppm.get_wireless_modem()
if modem then
log.info("BKPLN: found another wireless modem, using it for comms")
_bp.wl_nic.connect(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
else
databus.tx_hw_wl_modem(false)
end
else
print_no_fp("unassigned modem disconnected")
log.warning("BKPLN: unassigned modem disconnected")
end
end
end
return backplane

View File

@@ -18,8 +18,6 @@ local tri = util.trinary
local cpair = core.cpair
local self = {
tank_fluid_opts = {}, ---@type Radio2D[]
vis_draw = nil, ---@type function
draw_fluid_ops = nil, ---@type function
@@ -621,7 +619,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
if type == 0 then type = 1 end
self.tank_fluid_opts[i] = nil
tool_ctl.tank_fluid_opts[i] = nil
if tank_list[i] == 1 then
local row = Div{parent=tank_fluid_list,height=2}
@@ -636,7 +634,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
tank_fluid.disable()
end
self.tank_fluid_opts[i] = tank_fluid
tool_ctl.tank_fluid_opts[i] = tank_fluid
elseif tank_list[i] == 2 then
local row = Div{parent=tank_fluid_list,height=2}
@@ -661,7 +659,7 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
tank_fluid.disable()
end
self.tank_fluid_opts[i] = tank_fluid
tool_ctl.tank_fluid_opts[i] = tank_fluid
next_f = next_f + 1
end
@@ -676,11 +674,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
tmp_cfg.TankFluidTypes = {}
for i = 1, #tmp_cfg.FacilityTankList do
if self.tank_fluid_opts[i] ~= nil then
tmp_cfg.TankFluidTypes[i] = self.tank_fluid_opts[i].get_value()
else
tmp_cfg.TankFluidTypes[i] = 0
end
if tool_ctl.tank_fluid_opts[i] ~= nil then
tmp_cfg.TankFluidTypes[i] = tool_ctl.tank_fluid_opts[i].get_value()
else tmp_cfg.TankFluidTypes[i] = 0 end
end
fac_pane.set_value(8)

View File

@@ -1,4 +1,5 @@
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -14,6 +15,7 @@ local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
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")
@@ -25,14 +27,22 @@ local tri = util.trinary
local cpair = core.cpair
local LISTEN_MODE = types.LISTEN_MODE
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
update_net_cfg = nil, ---@type function
show_auth_key = nil, ---@type function
pkt_test = nil, ---@type Checkbox
pkt_chan = nil, ---@type NumberField
pkt_timeout = nil, ---@type NumberField
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
@@ -62,115 +72,230 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
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_c_5 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_6 = 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,net_c_4}}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4,net_c_5,net_c_6}}
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 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,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
local function on_wired_change(_) tool_ctl.gen_modem_list() end
TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_1,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
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)}
TextBox{parent=net_c_1,x=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
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="this one MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting it to peripherals will cause issues",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)}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"}
local rtu_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.get_value()
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
if not wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
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
self.update_net_cfg()
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 assign device connection interfaces if you selected multiple network interfaces."}
TextBox{parent=net_c_2,x=39,y=2,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_2,x=1,y=4,text="Reactor PLC\nRTU Gateway\nCoordinator",fg_bg=g_lg_fg_bg}
local opts = { "Wireless", "Wired", "Both" }
local plc_listen = Radio2D{parent=net_c_2,x=14,y=4,rows=1,columns=3,default=ini_cfg.PLC_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local rtu_listen = Radio2D{parent=net_c_2,x=14,rows=1,columns=3,default=ini_cfg.RTU_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local crd_listen = Radio2D{parent=net_c_2,x=14,rows=1,columns=3,default=ini_cfg.CRD_Listen,options=opts,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lightBlue,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
local function on_pocket_en(en)
if not en then
self.pkt_test.set_value(false)
self.pkt_test.disable()
else self.pkt_test.enable() end
end
TextBox{parent=net_c_2,y=8,text="With a wireless modem, configure Pocket access."}
local pkt_en = Checkbox{parent=net_c_2,y=10,label="Enable Pocket Access",default=ini_cfg.PocketEnabled,callback=on_pocket_en,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=24,y=10,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
self.pkt_test = Checkbox{parent=net_c_2,label="Enable Pocket Remote System Testing",default=ini_cfg.PocketTest,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=39,y=11,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=net_c_2,x=3,text="This allows remotely playing alarm sounds.",fg_bg=g_lg_fg_bg}
local function submit_net_cfg_opts()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
tmp_cfg.PLC_Listen = plc_listen.get_value()
tmp_cfg.RTU_Listen = rtu_listen.get_value()
tmp_cfg.CRD_Listen = crd_listen.get_value()
else
if tmp_cfg.WiredModem then
tmp_cfg.PLC_Listen = LISTEN_MODE.WIRED
tmp_cfg.RTU_Listen = LISTEN_MODE.WIRED
tmp_cfg.CRD_Listen = LISTEN_MODE.WIRED
else
tmp_cfg.PLC_Listen = LISTEN_MODE.WIRELESS
tmp_cfg.RTU_Listen = LISTEN_MODE.WIRELESS
tmp_cfg.CRD_Listen = LISTEN_MODE.WIRELESS
end
end
if tmp_cfg.WirelessModem then
tmp_cfg.PocketEnabled = pkt_en.get_value()
tmp_cfg.PocketTest = self.pkt_test.get_value()
else
tmp_cfg.PocketEnabled = false
tmp_cfg.PocketTest = false
end
if tmp_cfg.PocketEnabled then
self.pkt_chan.enable()
self.pkt_timeout.enable()
else
self.pkt_chan.disable()
self.pkt_timeout.disable()
end
net_pane.set_value(3)
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_net_cfg_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Each of the 5 uniquely named channels 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_3,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_3,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=9,width=11,text="PLC Channel"}
local plc_chan = NumberField{parent=net_c_3,x=21,y=9,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=9,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=10,width=19,text="RTU Gateway Channel"}
local rtu_chan = NumberField{parent=net_c_3,x=21,y=10,width=7,default=ini_cfg.RTU_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=10,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=11,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_3,x=21,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_3,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=12,width=14,text="Pocket Channel"}
self.pkt_chan = NumberField{parent=net_c_3,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_3,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, plc_c, rtu_c = tonumber(svr_chan.get_value()), tonumber(plc_chan.get_value()), tonumber(rtu_chan.get_value())
local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
local crd_c, pkt_c = tonumber(crd_chan.get_value()), tonumber(self.pkt_chan.get_value())
if not tmp_cfg.PocketEnabled then pkt_c = tmp_cfg.PKT_Channel or 16244 end
if svr_c ~= nil and plc_c ~= nil and rtu_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.PLC_Channel, tmp_cfg.RTU_Channel = svr_c, plc_c, rtu_c
tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = crd_c, pkt_c
net_pane.set_value(2)
net_pane.set_value(4)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
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_channels,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 connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_4,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,width=11,text="PLC Timeout"}
local plc_timeout = NumberField{parent=net_c_2,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=8,width=11,text="PLC Timeout"}
local plc_timeout = NumberField{parent=net_c_4,x=21,y=8,width=7,default=ini_cfg.PLC_Timeout,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=1,y=9,width=19,text="RTU Gateway Timeout"}
local rtu_timeout = NumberField{parent=net_c_2,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=9,width=19,text="RTU Gateway Timeout"}
local rtu_timeout = NumberField{parent=net_c_4,x=21,y=9,width=7,default=ini_cfg.RTU_Timeout,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=1,y=10,width=19,text="Coordinator Timeout"}
local crd_timeout = NumberField{parent=net_c_2,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=10,width=19,text="Coordinator Timeout"}
local crd_timeout = NumberField{parent=net_c_4,x=21,y=10,width=7,default=ini_cfg.CRD_Timeout,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=1,y=11,width=14,text="Pocket Timeout"}
local pkt_timeout = NumberField{parent=net_c_2,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,width=14,text="Pocket Timeout"}
self.pkt_timeout = NumberField{parent=net_c_4,x=21,y=11,width=7,default=ini_cfg.PKT_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=net_c_2,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=29,y=8,height=4,width=7,text="seconds\nseconds\nseconds\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local ct_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(pkt_timeout.get_value())
local plc_cto, rtu_cto, crd_cto, pkt_cto = tonumber(plc_timeout.get_value()), tonumber(rtu_timeout.get_value()), tonumber(crd_timeout.get_value()), tonumber(self.pkt_timeout.get_value())
if not tmp_cfg.PocketEnabled then pkt_cto = tmp_cfg.PKT_Timeout or 5 end
if plc_cto ~= nil and rtu_cto ~= nil and crd_cto ~= nil and pkt_cto ~= nil then
tmp_cfg.PLC_Timeout, tmp_cfg.RTU_Timeout, tmp_cfg.CRD_Timeout, tmp_cfg.PKT_Timeout = plc_cto, rtu_cto, crd_cto, pkt_cto
net_pane.set_value(3)
if tmp_cfg.WirelessModem then
net_pane.set_value(5)
else
tmp_cfg.TrustedRange = 0
tmp_cfg.AuthKey = ""
main_pane.set_value(4)
end
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,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_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,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=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=1,text="Please set the wireless trusted range below."}
TextBox{parent=net_c_5,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents wireless connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_5,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local range = NumberField{parent=net_c_5,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local tr_err = TextBox{parent=net_c_5,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
net_pane.set_value(4)
net_pane.set_value(6)
tr_err.hide(true)
else tr_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_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_5,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_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 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_6,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_6,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_4,x=1,y=11,text="Facility Auth Key"}
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}
TextBox{parent=net_c_6,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
local key, _ = TextField{parent=net_c_6,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_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
local hide_key = Checkbox{parent=net_c_6,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_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 key_err = TextBox{parent=net_c_6,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()
@@ -181,8 +306,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
else key_err.show() end
end
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}
PushButton{parent=net_c_6,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_6,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
@@ -195,7 +320,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
TextBox{parent=log_c_1,x=1,y=7,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
@@ -237,7 +362,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
local fp_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will be split up."}
@@ -374,15 +499,22 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.tank_mode, ini_cfg.FacilityTankMode)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(plc_listen, ini_cfg.PLC_Listen)
try_set(rtu_listen, ini_cfg.RTU_Listen)
try_set(crd_listen, ini_cfg.CRD_Listen)
try_set(pkt_en, ini_cfg.PocketEnabled)
try_set(self.pkt_test, ini_cfg.PocketTest)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(rtu_chan, ini_cfg.RTU_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(self.pkt_chan, ini_cfg.PKT_Channel)
try_set(plc_timeout, ini_cfg.PLC_Timeout)
try_set(rtu_timeout, ini_cfg.RTU_Timeout)
try_set(crd_timeout, ini_cfg.CRD_Timeout)
try_set(pkt_timeout, ini_cfg.PKT_Timeout)
try_set(self.pkt_timeout, ini_cfg.PKT_Timeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode)
@@ -406,6 +538,17 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
try_set(tool_ctl.aux_cool_elems[i].enable, ini_cfg.AuxiliaryCoolant[i])
end
for i = 1, #ini_cfg.TankFluidTypes do
if tool_ctl.tank_fluid_opts[i] then
if (ini_cfg.TankFluidTypes[i] > 0) then
tool_ctl.tank_fluid_opts[i].enable()
tool_ctl.tank_fluid_opts[i].set_value(ini_cfg.TankFluidTypes[i])
else
tool_ctl.tank_fluid_opts[i].disable()
end
end
end
tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
tool_ctl.view_cfg.enable()
@@ -470,6 +613,39 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
--#region Tool Functions
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- update the network interface configuration options
function self.update_net_cfg()
if tmp_cfg.WirelessModem and tmp_cfg.WiredModem then
plc_listen.enable()
rtu_listen.enable()
crd_listen.enable()
else
plc_listen.disable()
rtu_listen.disable()
crd_listen.disable()
end
if tmp_cfg.WirelessModem then
pkt_en.enable()
self.pkt_test.enable()
self.pkt_chan.enable()
self.pkt_timeout.enable()
else
pkt_en.set_value(false)
self.pkt_test.set_value(false)
pkt_en.disable()
self.pkt_test.disable()
self.pkt_chan.disable()
self.pkt_timeout.disable()
end
end
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("supervisor.config")
@@ -524,6 +700,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
tmp_cfg.FacilityTankList, tmp_cfg.FacilityTankConns = facility.generate_tank_list_and_conns(tmp_cfg.FacilityTankMode, tmp_cfg.FacilityTankDefs)
for i = 1, tmp_cfg.UnitCount do tmp_cfg.AuxiliaryCoolant[i] = false end
for i = 1, tmp_cfg.FacilityTankList do tmp_cfg.TankFluidTypes[i] = types.COOLANT_TYPE.WATER end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.RTU_Channel = config.RTU_CHANNEL
@@ -547,12 +726,6 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
self.importing_legacy = true
end
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- generate the summary list
---@param cfg svr_config
function tool_ctl.gen_summary(cfg)
@@ -675,6 +848,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end
if val == "" then val = "no auxiliary coolant" end
elseif f[1] == "PLC_Listen" or f[1] == "RTU_Listen" or f[1] == "CRD_Listen" then
if raw == LISTEN_MODE.WIRELESS then val = "Wireless Only"
elseif raw == LISTEN_MODE.WIRED then val = "Wired Only"
elseif raw == LISTEN_MODE.ALL then val = "Wireless and Wired" end
end
if not skip then
@@ -703,6 +880,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end
end
-- generate the list of available/assigned wired modems
function tool_ctl.gen_modem_list()
modem_list.remove_all()
local enable = wired.get_value()
local function select(iface)
tmp_cfg.WiredModem = iface
tool_ctl.gen_modem_list()
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 end
if 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

@@ -3,7 +3,9 @@
--
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local facility = require("supervisor.config.facility")
@@ -31,7 +33,8 @@ local CENTER = core.ALIGN.CENTER
local changes = {
{ "v1.2.12", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.6.0", { "Added sodium emergency coolant option" } }
{ "v1.6.0", { "Added sodium emergency coolant option" } },
{ "v1.8.0", { "Added support for wired communications modems", "Added option for allowing Pocket connections", "Added option for allowing Pocket test commands" } }
}
---@class svr_configurator
@@ -67,13 +70,16 @@ local tool_ctl = {
num_units = nil, ---@type NumberField
en_fac_tanks = nil, ---@type Checkbox
tank_mode = nil, ---@type RadioButton
tank_fluid_opts = {}, ---@type Radio2D[]
gen_summary = nil, ---@type function
load_legacy = nil, ---@type function
cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[]
tank_elems = {}, ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
aux_cool_elems = {} ---@type { line: Div, enable: Checkbox }[]
aux_cool_elems = {}, ---@type { line: Div, enable: Checkbox }[]
gen_modem_list = function () end
}
---@class svr_config
@@ -87,6 +93,13 @@ local tmp_cfg = {
TankFluidTypes = {}, ---@type integer[] which type of fluid each tank in the tank list should be containing
AuxiliaryCoolant = {}, ---@type boolean[] if a unit has auxiliary coolant
ExtChargeIdling = false,
WirelessModem = true,
WiredModem = false, ---@type string|false
PLC_Listen = 1, ---@type LISTEN_MODE
RTU_Listen = 1, ---@type LISTEN_MODE
CRD_Listen = 1, ---@type LISTEN_MODE
PocketEnabled = true,
PocketTest = true,
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer
@@ -121,6 +134,13 @@ local fields = {
{ "TankFluidTypes", "Tank Fluid Types", {} },
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
{ "ExtChargeIdling", "Extended Charge Idling", false },
{ "WirelessModem", "Wireless/Ender Comms Modem", true },
{ "WiredModem", "Wired Comms Modem", false },
{ "PLC_Listen", "PLC Listen Mode", types.LISTEN_MODE.WIRELESS },
{ "RTU_Listen", "RTU Gateway Listen Mode", types.LISTEN_MODE.WIRELESS },
{ "CRD_Listen", "Coordinator Listen Mode", types.LISTEN_MODE.WIRELESS },
{ "PocketEnabled", "Pocket Connectivity", true },
{ "PocketTest", "Pocket Testing Features", true },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 },
{ "RTU_Channel", "RTU Channel", 16242 },
@@ -286,11 +306,14 @@ function configurator.configure(ask_config)
tool_ctl.has_config = load_settings(ini_cfg)
-- these need to be initialized as they are used before being set
tmp_cfg.WiredModem = ini_cfg.WiredModem
tmp_cfg.FacilityTankMode = ini_cfg.FacilityTankMode
tmp_cfg.TankFluidTypes = { table.unpack(ini_cfg.TankFluidTypes) }
reset_term()
ppm.mount_all()
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
@@ -300,6 +323,8 @@ function configurator.configure(ask_config)
local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display)
tool_ctl.gen_modem_list()
while true do
local event, param1, param2, param3 = util.pull_event()
@@ -314,6 +339,14 @@ function configurator.configure(ask_config)
if k_e then display.handle_key(k_e) end
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_modem_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_modem_list()
end
if event == "terminate" then return end

View File

@@ -16,7 +16,7 @@ databus.ps = psil.create()
-- call to toggle heartbeat signal
function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
-- transmit firmware versions
---@param sv_v string supervisor version
---@param comms_v string comms version
function databus.tx_versions(sv_v, comms_v)
@@ -24,10 +24,16 @@ function databus.tx_versions(sv_v, comms_v)
databus.ps.publish("comms_version", comms_v)
end
-- transmit hardware status for modem connection state
-- transmit hardware status for the wired comms modem
---@param has_modem boolean
function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem)
function databus.tx_hw_wd_modem(has_modem)
databus.ps.publish("has_wd_modem", has_modem)
end
-- transmit hardware status for the wireless comms modem
---@param has_modem boolean
function databus.tx_hw_wl_modem(has_modem)
databus.ps.publish("has_wl_modem", has_modem)
end
-- transmit PLC firmware version and session connection state
@@ -166,11 +172,4 @@ function databus.tx_pdg_rtt(session_id, rtt)
end
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
databus.ps.subscribe(field, func)
end
return databus

View File

@@ -19,6 +19,7 @@ local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local TabBar = require("graphics.elements.controls.TabBar")
@@ -29,12 +30,14 @@ local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local ind_grn = style.ind_grn
-- create new front panel view
---@param panel DisplayBox main displaybox
local function init(panel)
---@param config svr_config configuraiton
local function init(panel, config)
local s_hi_box = style.theme.highlight_box
local s_hi_bright = style.theme.highlight_box_bright
@@ -53,7 +56,7 @@ local function init(panel)
local main_page = Div{parent=page_div,x=1,y=1}
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local system = Div{parent=main_page,width=18,height=17,x=2,y=2}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
@@ -62,25 +65,28 @@ local function init(panel)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
if config.WirelessModem and config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=ind_grn}
local wl_modem = LED{parent=system,label="WL MODEM",colors=ind_grn}
wd_modem.register(databus.ps, "has_wd_modem", wd_modem.update)
wl_modem.register(databus.ps, "has_wl_modem", wl_modem.update)
else
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
system.line_break()
modem.register(databus.ps, util.trinary(config.WirelessModem, "has_wl_modem", "has_wd_modem"), modem.update)
end
modem.register(databus.ps, "has_modem", modem.update)
--
-- hardware labeling
--
local hw_labels = Rectangle{parent=main_page,x=2,y=term_h-7,width=14,height=5,border=border(1,s_hi_box.bkg,true),even_inner=true}
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
local comp_id = util.sprintf("%03d", os.getComputerID())
--
-- about footer
--
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
TextBox{parent=hw_labels,text="FW "..databus.ps.get("version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="NT v"..databus.ps.get("comms_version"),fg_bg=s_hi_box}
TextBox{parent=hw_labels,text="SN "..comp_id.."-SVR",fg_bg=s_hi_box}
--
-- page handling

View File

@@ -19,15 +19,14 @@ local ui = {
}
-- try to start the UI
---@param theme FP_THEME front panel theme
---@param color_mode COLOR_MODE color mode
---@param config svr_config configuration
---@return boolean success, any error_msg
function renderer.try_start_ui(theme, color_mode)
function renderer.try_start_ui(config)
local status, msg = true, nil
if ui.display == nil then
-- set theme
style.set_theme(theme, color_mode)
style.set_theme(config.FrontPanelTheme, config.ColorMode)
-- reset terminal
term.setTextColor(colors.white)
@@ -41,7 +40,7 @@ function renderer.try_start_ui(theme, color_mode)
end
-- apply color mode
local c_mode_overrides = style.theme.color_modes[color_mode]
local c_mode_overrides = style.theme.color_modes[config.ColorMode]
for i = 1, #c_mode_overrides do
term.setPaletteColor(c_mode_overrides[i].c, c_mode_overrides[i].hex)
end
@@ -49,7 +48,7 @@ function renderer.try_start_ui(theme, color_mode)
-- init front panel view
status, msg = pcall(function ()
ui.display = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(ui.display)
panel_view(ui.display, config)
end)
if status then

View File

@@ -39,7 +39,8 @@ local PERIODICS = {
---@param sessions svsessions_list list of computer sessions, read-only
---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, sessions, facility, fp_ok)
---@param allow_test boolean if this should allow pocket testing commands
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, sessions, facility, fp_ok, allow_test)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
@@ -143,7 +144,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
local valid = false
-- attempt to set a tone state
if pkt.scada_frame.is_authenticated() then
if allow_test then
if pkt.length == 2 then
if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then
valid = true
@@ -151,22 +152,16 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
-- try to set tone states, then send back if testing is allowed
local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2])
_send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
else
log.debug(log_tag .. "SCADA diag tone set packet data type mismatch")
end
else
log.debug(log_tag .. "SCADA diag tone set packet length mismatch")
end
else
log.debug(log_tag .. "DIAG_TONE_SET is blocked without HMAC for security")
end
else log.debug(log_tag .. "SCADA diag tone set packet data type mismatch") end
else log.debug(log_tag .. "SCADA diag tone set packet length mismatch") end
else log.warning(log_tag .. "DIAG_TONE_SET is blocked without pocket test commands enabled") end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { false }) end
elseif pkt.type == MGMT_TYPE.DIAG_ALARM_SET then
local valid = false
-- attempt to set an alarm state
if pkt.scada_frame.is_authenticated() then
if allow_test then
if pkt.length == 2 then
if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then
valid = true
@@ -174,15 +169,9 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
-- try to set alarm states, then send back if testing is allowed
local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2])
_send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
else
log.debug(log_tag .. "SCADA diag alarm set packet data type mismatch")
end
else
log.debug(log_tag .. "SCADA diag alarm set packet length mismatch")
end
else
log.debug(log_tag .. "DIAG_ALARM_SET is blocked without HMAC for security")
end
else log.debug(log_tag .. "SCADA diag alarm set packet data type mismatch") end
else log.debug(log_tag .. "SCADA diag alarm set packet length mismatch") end
else log.warning(log_tag .. "DIAG_ALARM_SET is blocked without pocket test commands enabled") end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end
elseif pkt.type == MGMT_TYPE.INFO_LIST_CMP then

View File

@@ -2,6 +2,7 @@
-- Supervisor Sessions Handler
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
@@ -41,9 +42,8 @@ svsessions.SESSION_TYPE = SESSION_TYPE
local self = {
-- references to supervisor state and other data
nic = nil, ---@type nic|nil
fp_ok = false,
config = nil, ---@type svr_config
config = nil, ---@type svr_config|nil
facility = nil, ---@type facility|nil
plc_ini_reset = {},
-- lists of connected sessions
@@ -55,7 +55,6 @@ local self = {
crd = {}, ---@type crd_session_struct[]
pdg = {} ---@type pdg_session_struct[]
},
---@diagnostic enable: missing-fields
-- next session IDs
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 },
-- rtu device tracking and invalid assignment detection
@@ -84,7 +83,7 @@ local function _sv_handle_outq(session)
if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent
self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
session.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then
@@ -140,12 +139,9 @@ end
local function _iterate(sessions)
for i = 1, #sessions do
local session = sessions[i]
if session.open and session.instance.iterate() then
_sv_handle_outq(session)
else
session.open = false
end
else session.open = false end
end
end
@@ -159,7 +155,7 @@ local function _shutdown(session)
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
session.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
end
end
@@ -359,12 +355,10 @@ function svsessions.check_rtu_id(unit, list, max)
end
-- initialize svsessions
---@param nic nic network interface device
---@param fp_ok boolean front panel active
---@param config svr_config supervisor configuration
---@param facility facility
function svsessions.init(nic, fp_ok, config, facility)
self.nic = nic
function svsessions.init(fp_ok, config, facility)
self.fp_ok = fp_ok
self.config = config
self.facility = facility
@@ -467,19 +461,24 @@ end
-- establish a new PLC session
---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer PLC computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param for_reactor integer unit ID
---@param version string PLC version
---@return integer|false session_id
function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, version)
---@return integer|boolean session_id session ID, false if unit is already connected, and true if this is a successful connection test
function svsessions.establish_plc_session(nic, source_addr, i_seq_num, for_reactor, version)
if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then
-- don't actually establish this if it is a connection test
if version == comms.CONN_TEST_FWV then return true end
---@class plc_session_struct
local plc_s = {
s_type = "plc",
open = true,
reactor = for_reactor,
version = version,
nic = nic,
r_chan = self.config.PLC_Channel,
s_addr = source_addr,
in_queue = mqueue.new(),
@@ -517,17 +516,19 @@ end
-- establish a new RTU gateway session
---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer RTU gateway computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param advertisement table RTU capability advertisement
---@param version string RTU gateway version
---@return integer session_id
function svsessions.establish_rtu_session(source_addr, i_seq_num, advertisement, version)
function svsessions.establish_rtu_session(nic, source_addr, i_seq_num, advertisement, version)
---@class rtu_session_struct
local rtu_s = {
s_type = "rtu",
open = true,
version = version,
nic = nic,
r_chan = self.config.RTU_Channel,
s_addr = source_addr,
in_queue = mqueue.new(),
@@ -558,17 +559,19 @@ end
-- establish a new coordinator session
---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer coordinator computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string coordinator version
---@return integer|false session_id
function svsessions.establish_crd_session(source_addr, i_seq_num, version)
function svsessions.establish_crd_session(nic, source_addr, i_seq_num, version)
if svsessions.get_crd_session() == nil then
---@class crd_session_struct
local crd_s = {
s_type = "crd",
open = true,
version = version,
nic = nic,
r_chan = self.config.CRD_Channel,
s_addr = source_addr,
in_queue = mqueue.new(),
@@ -603,16 +606,18 @@ end
-- establish a new pocket diagnostics session
---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version
---@return integer|false session_id
function svsessions.establish_pdg_session(source_addr, i_seq_num, version)
function svsessions.establish_pdg_session(nic, source_addr, i_seq_num, version)
---@class pdg_session_struct
local pdg_s = {
s_type = "pkt",
open = true,
version = version,
nic = nic,
r_chan = self.config.PKT_Channel,
s_addr = source_addr,
in_queue = mqueue.new(),
@@ -622,7 +627,7 @@ function svsessions.establish_pdg_session(source_addr, i_seq_num, version)
local id = self.next_ids.pdg
pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.sessions, self.facility, self.fp_ok)
pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.sessions, self.facility, self.fp_ok, self.config.PocketTest)
table.insert(self.sessions.pdg, pdg_s)
local mt = {

View File

@@ -15,6 +15,7 @@ local util = require("scada-common.util")
local core = require("graphics.core")
local backplane = require("supervisor.backplane")
local configure = require("supervisor.configure")
local databus = require("supervisor.databus")
local facility = require("supervisor.facility")
@@ -23,7 +24,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.7.1"
local SUPERVISOR_VERSION = "v1.8.0"
local println = util.println
local println_ts = util.println_ts
@@ -114,7 +115,7 @@ local function main()
-- startup
----------------------------------------
-- record firmware versions and ID
-- report versions
databus.tx_versions(SUPERVISOR_VERSION, comms.version)
-- mount connected devices
@@ -125,18 +126,11 @@ local function main()
network.init_mac(config.AuthKey)
end
-- get modem
local modem = ppm.get_wireless_modem()
if modem == nil then
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- modem initialization
if not backplane.init(config) then return end
-- start UI
local fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
local fp_ok, message = renderer.try_start_ui(config)
if not fp_ok then
println_ts(util.c("UI error: ", message))
@@ -150,8 +144,7 @@ local function main()
local sv_facility = facility.new(config)
-- create network interface then setup comms
local nic = network.nic(modem)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok, sv_facility)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, fp_ok, sv_facility)
-- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15
@@ -173,49 +166,13 @@ local function main()
-- handle event
if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only care if this is our wireless modem
if nic.is_modem(device) then
nic.disconnect()
println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem()
if other_modem then
log.info("found another wireless modem, using it for comms")
nic.connect(other_modem)
else
databus.tx_hw_modem(false)
end
else
log.warning("non-comms modem disconnected")
end
end
backplane.detach(param1, type, device, println_ts)
end
elseif event == "peripheral" then
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
nic.connect(device)
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
databus.tx_hw_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log.info("wired modem reconnected")
end
end
backplane.attach(param1, type, device, println_ts)
end
elseif event == "timer" and loop_clock.is_clock(param1) then
-- main loop tick

View File

@@ -1,9 +1,12 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local themes = require("graphics.themes")
local backplane = require("supervisor.backplane")
local svsessions = require("supervisor.session.svsessions")
local supervisor = {}
@@ -11,8 +14,11 @@ local supervisor = {}
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local PROBE_ACK = comms.PROBE_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local LISTEN_MODE = types.LISTEN_MODE
---@type svr_config
---@diagnostic disable-next-line: missing-fields
local config = {}
@@ -47,6 +53,16 @@ function supervisor.load_config()
config.AuxiliaryCoolant = settings.get("AuxiliaryCoolant")
config.ExtChargeIdling = settings.get("ExtChargeIdling")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PLC_Listen = settings.get("PLC_Listen")
config.RTU_Listen = settings.get("RTU_Listen")
config.CRD_Listen = settings.get("CRD_Listen")
config.PocketEnabled = settings.get("PocketEnabled")
config.PocketTest = settings.get("PocketTest")
config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel")
config.RTU_Channel = settings.get("RTU_Channel")
@@ -84,6 +100,20 @@ function supervisor.load_config()
cfv.assert_type_bool(config.ExtChargeIdling)
cfv.assert_type_bool(config.WirelessModem)
cfv.assert((config.WiredModem == false) or (type(config.WiredModem) == "string"))
cfv.assert((config.WirelessModem == true) or (type(config.WiredModem) == "string"))
cfv.assert_type_int(config.PLC_Listen)
cfv.assert_range(config.PLC_Listen, 1, 3)
cfv.assert_type_int(config.RTU_Listen)
cfv.assert_range(config.RTU_Listen, 1, 3)
cfv.assert_type_int(config.CRD_Listen)
cfv.assert_range(config.CRD_Listen, 1, 3)
cfv.assert_type_bool(config.PocketEnabled)
cfv.assert_type_bool(config.PocketTest)
cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.PLC_Channel)
cfv.assert_channel(config.RTU_Channel)
@@ -123,36 +153,33 @@ end
-- supervisory controller communications
---@nodiscard
---@param _version string supervisor version
---@param nic nic network interface device
---@param fp_ok boolean if the front panel UI is running
---@param facility facility facility instance
---@diagnostic disable-next-line: unused-local
function supervisor.comms(_version, nic, fp_ok, facility)
function supervisor.comms(_version, fp_ok, facility)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local self = {
last_est_acks = {}
last_est_acks = {} ---@type ESTABLISH_ACK[]
}
if config.WirelessModem then
comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS --
-- configure modem channels
nic.closeAll()
nic.open(config.SVR_Channel)
end
-- pass system data and objects to svsessions
svsessions.init(nic, fp_ok, config, facility)
svsessions.init(fp_ok, config, facility)
--#region PRIVATE FUNCTIONS --
-- send an establish request response
---@param nic nic
---@param packet scada_packet
---@param ack ESTABLISH_ACK
---@param data? any optional data
local function _send_establish(packet, ack, data)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
local function _send_establish(nic, packet, ack, data)
local s_pkt, m_pkt = comms.scada_packet(), comms.mgmt_packet()
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
@@ -161,7 +188,209 @@ function supervisor.comms(_version, nic, fp_ok, facility)
self.last_est_acks[packet.src_addr()] = ack
end
-- PUBLIC FUNCTIONS --
-- send a probe response
---@param nic nic
---@param packet scada_packet
---@param ack PROBE_ACK
local function _send_probe(nic, packet, ack)
local s_pkt, m_pkt = comms.scada_packet(), comms.mgmt_packet()
m_pkt.make(MGMT_TYPE.PROBE, { ack })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
nic.transmit(packet.remote_channel(), config.SVR_Channel, s_pkt)
end
--#region Establish Handlers
-- handle a PLC establish
---@param nic nic
---@param packet mgmt_frame
---@param src_addr integer
---@param i_seq_num integer
---@param last_ack ESTABLISH_ACK
local function _establish_plc(nic, packet, src_addr, i_seq_num, last_ack)
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if (config.PLC_Listen ~= LISTEN_MODE.ALL) and (nic.isWireless() ~= (config.PLC_Listen == LISTEN_MODE.WIRELESS)) and periphemu == nil then
-- drop if not listening
elseif comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("PLC_ESTABLISH: PLC [@", src_addr, "] dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
-- check ID validity
if reactor_id < 1 or reactor_id > config.UnitCount then
-- reactor index out of range
if last_ack ~= ESTABLISH_ACK.DENY then
log.warning(util.c("PLC_ESTABLISH: PLC [@", src_addr, "] denied assignment ", reactor_id, " outside of configured unit count ", config.UnitCount))
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
else
-- try to establish the session
local plc_id = svsessions.establish_plc_session(nic, src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: PLC [@", src_addr, "] assignment collision with reactor ", reactor_id))
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.COLLISION)
elseif plc_id == true then
-- valid, but this was just a test
log.info(util.c("PLC_ESTABLISH: PLC [@", src_addr, "] sending connection test success response on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC [@", src_addr, "] (", firmware_v, ") reactor unit ", reactor_id, " PLC connected with session ID ", plc_id, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
end
else
log.debug("PLC_ESTABLISH: [@" .. src_addr .. "] packet length mismatch/bad parameter type")
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("PLC_ESTABLISH: [@", src_addr, "] illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
end
-- handle an RTU gateway establish
---@param nic nic
---@param packet mgmt_frame
---@param src_addr integer
---@param i_seq_num integer
---@param last_ack ESTABLISH_ACK
local function _establish_rtu_gw(nic, packet, src_addr, i_seq_num, last_ack)
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if (config.RTU_Listen ~= LISTEN_MODE.ALL) and (nic.isWireless() ~= (config.RTU_Listen == LISTEN_MODE.WIRELESS)) and periphemu == nil then
-- drop if not listening
elseif comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("RTU_GW_ESTABLISH: [@", src_addr, "] dropping RTU_GW establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
if firmware_v ~= comms.CONN_TEST_FWV then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
local s_id = svsessions.establish_rtu_session(nic, src_addr, i_seq_num, rtu_advert, firmware_v)
println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_GW_ESTABLISH: [@", src_addr, "] RTU_GW (",firmware_v, ") connected with session ID ", s_id, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
-- valid, but this was just a test
log.info(util.c("RTU_GW_ESTABLISH: RTU_GW [@", src_addr, "] sending connection test success response on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
else
log.debug("RTU_GW_ESTABLISH: [@" .. src_addr .. "] packet length mismatch")
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("RTU_GW_ESTABLISH: [@", src_addr, "] illegal establish packet for device ", dev_type, " on RTU channel"))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
end
-- handle a coordinator establish
---@param nic nic
---@param packet mgmt_frame
---@param src_addr integer
---@param i_seq_num integer
---@param last_ack ESTABLISH_ACK
local function _establish_crd(nic, packet, src_addr, i_seq_num, last_ack)
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if (config.CRD_Listen ~= LISTEN_MODE.ALL) and (nic.isWireless() ~= (config.CRD_Listen == LISTEN_MODE.WIRELESS)) and periphemu == nil then
-- drop if not listening
elseif comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("CRD_ESTABLISH: [@", src_addr, "] dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.CRD then
-- this is an attempt to establish a new coordinator session
local s_id = svsessions.establish_crd_session(nic, src_addr, i_seq_num, firmware_v)
if s_id ~= false then
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: [@", src_addr, "] CRD (", firmware_v, ") connected with session ID ", s_id, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, facility.get_cooling_conf() })
else
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: [@" .. src_addr .. "] denied new coordinator due to already being connected to another coordinator")
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.COLLISION)
end
else
log.debug(util.c("CRD_ESTABLISH: [@", src_addr, "] illegal establish packet for device ", dev_type, " on CRD channel"))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
end
-- handle a pocket debug establish
---@param nic nic
---@param packet mgmt_frame
---@param src_addr integer
---@param i_seq_num integer
---@param last_ack ESTABLISH_ACK
local function _establish_pdg(nic, packet, src_addr, i_seq_num, last_ack)
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if not config.PocketEnabled then
-- drop if not listening
elseif comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("PDG_ESTABLISH: [@", src_addr, "] dropping PKT establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_pdg_session(nic, src_addr, i_seq_num, firmware_v)
println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("PDG_ESTABLISH: [@", src_addr, "] pocket (", firmware_v, ") connected with session ID ", s_id, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("PDG_ESTABLISH: [@", src_addr, "] illegal establish packet for device ", dev_type, " on PKT channel"))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
end
--#endregion
--#endregion
--#region PUBLIC FUNCTIONS --
---@class superv_comms
local public = {}
@@ -175,36 +404,33 @@ function supervisor.comms(_version, nic, fp_ok, facility)
---@param distance integer
---@return modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil
local pkt, s_pkt, nic = nil, nil, backplane.nics[side]
if nic then
s_pkt = nic.receive(side, sender, reply_to, message, distance)
else
log.error("parse_packet(" .. side .. "): received a packet from an interface without a nic?")
end
if s_pkt then
-- get as MODBUS TCP packet
if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
local m_pkt = comms.modbus_packet()
if m_pkt.decode(s_pkt) then
pkt = m_pkt.get()
end
if m_pkt.decode(s_pkt) then pkt = m_pkt.get() end
-- get as RPLC packet
elseif s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get()
end
if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end
-- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end
-- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
end
if crdn_pkt.decode(s_pkt) then pkt = crdn_pkt.get() end
else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
log.debug("parse_packet(" .. side .. "): attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
@@ -214,6 +440,7 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- handle a packet
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame
function public.handle_packet(packet)
local nic = backplane.nics[packet.scada_frame.interface()]
local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr()
@@ -226,81 +453,39 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- look for an associated session
local session = svsessions.find_plc_session(src_addr)
if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- reactor PLC packet
if session ~= nil then
if session then
if nic ~= session.nic then
-- this is from the same device but on a different interface
-- drop unless it is a connection probe
if (protocol == PROTOCOL.SCADA_MGMT) and (packet.type == MGMT_TYPE.PROBE) then
---@cast packet mgmt_frame
log.debug(util.c("PROBE_ACK: conflict with PLC @", src_addr, " on ", session.nic.phy_name(), " probed on ", nic.phy_name()))
_send_probe(nic, packet.scada_frame, PROBE_ACK.CONFLICT)
else
log.debug(util.c("unexpected packet for PLC @ ", src_addr, " received on ", nic.phy_name()))
end
else
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding RPLC packet without a known session")
end
elseif protocol == PROTOCOL.RPLC then
-- reactor PLC packet should be session related, discard it
log.debug("discarding RPLC packet without a known session")
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session: validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
-- check ID validity
if reactor_id < 1 or reactor_id > config.UnitCount then
-- reactor index out of range
if last_ack ~= ESTABLISH_ACK.DENY then
log.warning(util.c("PLC_ESTABLISH: denied assignment ", reactor_id, " outside of configured unit count ", config.UnitCount))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
else
-- try to establish the session
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
_establish_plc(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
else
log.debug("invalid establish packet (on PLC channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
elseif packet.type == MGMT_TYPE.PROBE then
-- connection probing
log.debug(util.c("PROBE_ACK: reporting open to PLC @", src_addr, " probed on ", nic.phy_name()))
_send_probe(nic, packet.scada_frame, PROBE_ACK.OPEN)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
@@ -312,62 +497,43 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- look for an associated session
local session = svsessions.find_rtu_session(src_addr)
if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- MODBUS response
if session ~= nil then
if session then
if nic ~= session.nic then
-- this is from the same device but on a different interface
-- drop unless it is a connection probe
if (protocol == PROTOCOL.SCADA_MGMT) and (packet.type == MGMT_TYPE.PROBE) then
---@cast packet mgmt_frame
log.debug(util.c("PROBE_ACK: conflict with RTU_GW @", src_addr, " on ", session.nic.phy_name(), " probed on ", nic.phy_name()))
_send_probe(nic, packet.scada_frame, PROBE_ACK.CONFLICT)
else
log.debug(util.c("unexpected packet for RTU_GW @ ", src_addr, " received on ", nic.phy_name()))
end
else
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
elseif protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- MODBUS response, should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
if packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session: validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
local s_id = svsessions.establish_rtu_session(src_addr, i_seq_num, rtu_advert, firmware_v)
println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug("RTU_ESTABLISH: packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
_establish_rtu_gw(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
else
log.debug("invalid establish packet (on RTU channel)")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
elseif packet.type == MGMT_TYPE.PROBE then
-- connection probing
log.debug(util.c("PROBE_ACK: reporting open to RTU_GW @", src_addr, " probed on ", nic.phy_name()))
_send_probe(nic, packet.scada_frame, PROBE_ACK.OPEN)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr))
log.debug(util.c("discarding RTU gateway SCADA_MGMT packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
@@ -376,110 +542,64 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- look for an associated session
local session = svsessions.find_crd_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
if session then
if nic ~= session.nic then
-- this is from the same device but on a different interface
-- drop unless it is a connection probe
if (protocol == PROTOCOL.SCADA_MGMT) and (packet.type == MGMT_TYPE.PROBE) then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
log.debug(util.c("PROBE_ACK: conflict with CRD @", src_addr, " on ", session.nic.phy_name(), " probed on ", nic.phy_name()))
_send_probe(nic, packet.scada_frame, PROBE_ACK.CONFLICT)
else
log.debug(util.c("unexpected packet for CRD @ ", src_addr, " received on ", nic.phy_name()))
end
else
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session: validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.CRD then
-- this is an attempt to establish a new coordinator session
local s_id = svsessions.establish_crd_session(src_addr, i_seq_num, firmware_v)
if s_id ~= false then
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, facility.get_cooling_conf() })
else
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
end
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
_establish_crd(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
else
log.debug("CRD_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
elseif packet.type == MGMT_TYPE.PROBE then
-- connection probing
log.debug(util.c("PROBE_ACK: reporting open to CRD @", src_addr, " probed on ", nic.phy_name()))
_send_probe(nic, packet.scada_frame, PROBE_ACK.OPEN)
else
-- any other packet should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr))
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
-- coordinator packet, should be session related, discard it
log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
log.debug(util.c("illegal packet type ", protocol, " on CRD channel"))
end
elseif r_chan == config.PKT_Channel then
-- look for an associated session
local session = svsessions.find_pdg_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
if session then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session
local last_ack = self.last_est_acks[src_addr]
-- validate packet and continue
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet
if packet.type == MGMT_TYPE.ESTABLISH then
-- establish a new session: validate packet and continue
if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1]
local firmware_v = packet.data[2]
local dev_type = packet.data[3]
if comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
local s_id = svsessions.establish_pdg_session(src_addr, i_seq_num, firmware_v)
println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel"))
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end
_establish_pdg(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
else
log.debug("PDG_ESTABLISH: establish packet length mismatch")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
-- any other packet should be session related, discard it
@@ -487,14 +607,8 @@ function supervisor.comms(_version, nic, fp_ok, facility)
end
elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
else
-- any other packet should be session related, discard it
-- coordinator packet, should be session related, discard it
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
end
@@ -503,6 +617,8 @@ function supervisor.comms(_version, nic, fp_ok, facility)
end
end
--#endregion
return public
end