Compare commits

...

50 Commits

Author SHA1 Message Date
Mikayla Fischler
cddd9f7437 #634 work on reactor PLC backplane 2025-10-26 22:19:37 -04:00
Mikayla Fischler
5acc6470e3 PPM parameter clarity 2025-10-26 20:41:25 -04:00
Mikayla Fischler
e57c6205e2 comment cleanup 2025-10-26 20:41:02 -04:00
Mikayla Fischler
96acb03f73 #580 reactor PLC wired/wireless configurator updates, RTU gateway and supervisor updates to theirs as well 2025-10-26 20:31:35 -04:00
Mikayla Fischler
2fefe4fbd6 version increment and log message formatting 2025-10-26 18:15:18 -04:00
Mikayla Fischler
1f9e86f6ea Merge branch '580-wired-comms-networking' of github.com:MikaylaFischler/cc-mek-scada into 580-wired-comms-networking 2025-10-26 18:02:12 -04:00
Mikayla Fischler
a48c8c1efe merge fixes 2025-10-26 17:22:07 -04:00
Mikayla Fischler
deeeb612b1 Merge branch 'devel' into 580-wired-comms-networking 2025-10-26 17:08:12 -04:00
Mikayla
c62eaeb5a2 #580 work on reactor PLC wired comms 2025-10-26 20:54:09 +00:00
Mikayla
1a7cb9eaa8
Merge pull request #641 from MikaylaFischler/640-plc-initialization-enhancements
640 plc initialization enhancements
2025-10-26 16:50:29 -04:00
Mikayla Fischler
3139dc2176 review updates 2025-10-26 16:49:19 -04:00
Mikayla Fischler
25fc0050c3 fixed trailing whitespace 2025-10-26 16:06:16 -04:00
Mikayla
b57aceff15 comment updates 2025-10-26 20:05:19 +00:00
Mikayla
8fd04e44f3 #580 require a modem config in RTU gateway config check 2025-10-26 20:04:27 +00:00
Mikayla
869b342db2 #634 supervisor backplane logging changes 2025-10-26 20:03:42 +00:00
Mikayla Fischler
f0251efec6 #580 RTU backplane and logging updates 2025-10-26 15:41:43 -04:00
Mikayla Fischler
c7e02efbc7 #580 RTU configurator updates 2025-10-26 15:39:30 -04:00
Mikayla Fischler
db8bed583f #580 additional supervisor configurator fixes 2025-10-26 15:34:47 -04:00
Mikayla Fischler
2d44014e2e #580 fixes for listen mode enum change 2025-10-26 15:30:14 -04:00
Mikayla Fischler
452fe71ab8 #580 modem list fix 2025-10-26 15:28:30 -04:00
Mikayla Fischler
22208e91aa #580 supervisor network configurator updates 2025-10-26 15:17:15 -04:00
Mikayla Fischler
d412f61a5f tool_ctl tank_fluid_opts for prior commit 2025-10-26 15:16:02 -04:00
Mikayla Fischler
c62ec1e786 include tank fluid types and aux coolant correctly in load_legacy and save_and_continue 2025-10-26 15:02:03 -04:00
Mikayla Fischler
1890f0a983 remove needless empty graphics callbacks 2025-10-26 14:21:40 -04:00
Mikayla Fischler
390cf98b0a private scada-common data refactor 2025-10-26 13:41:59 -04:00
Mikayla Fischler
7ddd6f32c5 more log tags 2025-10-26 13:39:58 -04:00
Mikayla Fischler
18a488f1b9 #640 formed bugfix 2025-10-26 13:38:37 -04:00
Mikayla Fischler
4c7ad0c539 #640 bug fixes and enhancements 2025-10-24 15:30:26 -04:00
Mikayla
a083f8983b #640 reworked PLC initialization 2025-10-23 23:13:13 +00:00
Mikayla Fischler
cb11ece73d #580 WIP updated supervisor network config 2025-10-19 20:28:34 -04:00
Mikayla Fischler
fc24f39991 #580 supervisor wired/wireless dual networking 2025-10-19 17:30:05 -04:00
Mikayla Fischler
4d6c388f37 #580 supervisor backplane 2025-10-19 15:19:30 -04:00
Mikayla Fischler
9e3922a972 ppm wired modem list 2025-10-18 18:38:42 -04:00
Mikayla Fischler
1fcc91e98b #580 RTU gateway multi-modem wired/wireless failover networking 2025-10-18 17:23:06 -04:00
Mikayla Fischler
fe9ee313f9 fixed Checkbox set_value type hint 2025-10-18 17:17:33 -04:00
Mikayla Fischler
194a266730 #580 RTU gateway backplane 2025-10-18 14:44:33 -04:00
Mikayla Fischler
88862726e3 migrate RTU initialization to new file 2025-10-18 12:37:27 -04:00
Mikayla
2aa5c93404 comment and formatting fixes 2025-10-17 20:59:36 +00:00
Mikayla
859e04712f Merge branch 'devel' into 580-wired-comms-networking 2025-09-30 13:08:45 +00:00
Mikayla
9591668f87 #580 supervisor comms config verification 2025-06-28 17:57:47 +00:00
Mikayla
4a38ca7dd1 #580 nic constructor simplification 2025-06-28 17:41:25 +00:00
Mikayla
250db00794 #580 supervisor front panel updates for wired modem support 2025-06-28 17:26:49 +00:00
Mikayla
391b68d357 #580 supervisor wired comms networking logic 2025-06-28 17:17:31 +00:00
Mikayla Fischler
4a7fc6200e #580 work on supervisor wired modem configuration 2025-06-15 15:43:04 -04:00
Mikayla
bee1cdf01c return wireless modem interface name 2025-05-24 20:03:17 +00:00
Mikayla
c6143934d8 check interface side in network logic before handling packets 2025-05-24 20:02:19 +00:00
Mikayla
c319039a4e simplified RTU gateway startup check failure code 2025-05-24 20:01:05 +00:00
Mikayla
4b61037170 Merge branch 'devel' into 580-wired-comms-networking 2025-05-19 13:26:36 +00:00
Mikayla
028a161af0 #580 RTU gateway changes for wired comms modems 2025-05-05 17:57:54 +00:00
Mikayla
454d166ac9 #580 reactor PLC changes for wired comms modems 2025-05-05 17:54:47 +00:00
43 changed files with 2951 additions and 1694 deletions

View File

@ -237,17 +237,17 @@ 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=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"} 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=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 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"} 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"} 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() local function submit_ui_opts()
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1 tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1

View File

@ -188,7 +188,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=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} 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"} 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} 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 +230,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=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"} 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"} 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."} 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."}

View File

@ -20,8 +20,8 @@ local log_comms = coordinator.log_comms
local threads = {} local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RENDER_SLEEP = 100 -- (100ms, 2 ticks) local RENDER_SLEEP = 100 -- 100ms, 2 ticks
local MQ__RENDER_CMD = { local MQ__RENDER_CMD = {
START_MAIN_UI = 1, START_MAIN_UI = 1,

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 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) 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_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 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) group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)

View File

@ -102,7 +102,7 @@ return function (args)
end end
-- set the value -- set the value
---@param val integer new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
e.value = val e.value = val
draw() 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,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 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} 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=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"} 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"} 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() local function submit_ui_units()
tmp_cfg.TempScale = temp_scale.get_value() 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=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} 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"} 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} 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

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

View File

@ -162,7 +162,7 @@ local function new_view(root)
TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER} 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 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) mode.register(f_ps, "process_mode", mode.set_value)

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

@ -0,0 +1,236 @@
--
-- 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 databus = require("reactor-plc.databus")
local plc = require("reactor-plc.plc")
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
wl_act = true,
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
-- Modem Init
if _bp.smem.networked then
-- init wired NIC
if type(config.WiredModem) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
_bp.wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. _bp.lan_iface)
plc_state.wd_modem = _bp.wd_nic.is_connected()
-- set this as active for now
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
_bp.wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_" .. util.trinary(modem, "UP ", "DOWN ") .. iface)
plc_state.wl_modem = _bp.wl_nic.is_connected()
-- 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.wl_act = true
_bp.act_nic = _bp.wl_nic
end
end
-- comms modem is required if networked
if not (plc_state.wd_modem or plc_state.wl_modem) then
println("startup> comms modem not 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
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")
elseif not plc_dev.reactor.isFormed() then
println("startup> fission reactor is not formed")
log.warning("BKPLN: reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
else
log.info("BKPLN: reactor detected")
end
end
-- get the active NIC
---@return 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_cmds.MQ__RPS_CMD
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
dev.reactor = device
state.no_reactor = false
print_no_fp("reactor reconnected")
log.info("BKPLN: 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
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))
local is_wd = _bp.wd_nic and (_bp.lan_iface == iface)
local is_wl = _bp.wl_nic and (not _bp.wl_nic.is_connected()) and m_is_wl
if is_wd 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")
state.wd_modem = true
if _bp.act_nic == _bp.wd_nic then
-- set as active
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
elseif _bp.wl_act and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif is_wl then
-- connect this as the wireless NIC
_bp.wl_nic.connect(device)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
print_no_fp("wireless comms modem reconnected")
state.wl_modem = true
if _bp.act_nic == _bp.wl_nic then
-- set as active
_bp.wl_act = true
_bp.act_nic = _bp.wl_nic
elseif (not _bp.wl_act) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.wl_act = true
_bp.act_nic = _bp.wl_nic
sys.plc_comms.switch_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif _bp.wl_nic and m_is_wl then
-- the wireless NIC already has a modem
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
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)
end
return backplane

View File

@ -1,4 +1,5 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local util = require("scada-common.util") 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 IndLight = require("graphics.elements.indicators.IndicatorLight")
local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT local RIGHT = core.ALIGN.RIGHT
@ -30,6 +33,8 @@ local self = {
set_networked = nil, ---@type function set_networked = nil, ---@type function
bundled_emcool = nil, ---@type function bundled_emcool = nil, ---@type function
wl_pref = nil, ---@type Checkbox
range = nil, ---@type NumberField
show_auth_key = nil, ---@type function show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox auth_key_textbox = nil, ---@type TextBox
@ -154,14 +159,14 @@ 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 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"} 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} 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=10,y=3,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision 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
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)} 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} 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() local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] 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() tmp_cfg.EmerCoolInvert = invert.get_value()
next_from_plc() next_from_plc()
end end
@ -177,22 +182,77 @@ 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_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_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_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_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=1,text="Please select the network interface(s)."}
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=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 function dis_pref(value)
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} if not value then
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} self.wl_pref.set_value(false)
TextBox{parent=net_c_1,x=1,y=11,text="PLC Channel"} self.wl_pref.disable()
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} else self.wl_pref.enable() end
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg} 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(_) tool_ctl.gen_modem_list() end
local wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=dis_pref}
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}
local wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting to peripherals will cause problems",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
dis_pref(ini_cfg.WirelessModem)
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.get_value()
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem and self.wl_pref.get_value()
if not wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (wired.get_value() or wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
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 function submit_channels()
local svr_c = tonumber(svr_chan.get_value()) local svr_c = tonumber(svr_chan.get_value())
@ -200,7 +260,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if svr_c ~= nil and plc_c ~= nil then if svr_c ~= nil and plc_c ~= nil then
tmp_cfg.SVR_Channel = svr_c tmp_cfg.SVR_Channel = svr_c
tmp_cfg.PLC_Channel = plc_c tmp_cfg.PLC_Channel = plc_c
net_pane.set_value(2) net_pane.set_value(3)
chan_err.hide(true) chan_err.hide(true)
elseif svr_c == nil then elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.") chan_err.set_value("Please set the supervisor channel.")
@ -211,54 +271,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
end end
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_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_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=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"} TextBox{parent=net_c_3,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} 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_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_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_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=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"} TextBox{parent=net_c_3,x=1,y=8,text="Trusted Range (Wireless Only)"}
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} 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_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=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 function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value()) local timeout_val = tonumber(timeout.get_value())
local range_val = tonumber(range.get_value()) local range_val = tonumber(self.range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val if timeout_val == nil then
tmp_cfg.TrustedRange = range_val n3_err.set_value("Please set the connection timeout.")
net_pane.set_value(3) n3_err.show()
p2_err.hide(true) elseif tmp_cfg.WirelessModem and (range_val == nil) then
elseif timeout_val == nil then n3_err.set_value("Please set the trusted range.")
p2_err.set_value("Please set the connection timeout.") n3_err.show()
p2_err.show()
else else
p2_err.set_value("Please set the trusted range.") tmp_cfg.ConnTimeout = timeout_val
p2_err.show() 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
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_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_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=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_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_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=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"} TextBox{parent=net_c_4,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
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} 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) hide_key.set_value(true)
censor_key(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 function submit_auth()
local v = key.get_value() local v = key.get_value()
@ -269,8 +337,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
else key_err.show() end else key_err.show() end
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_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_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=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
@ -283,7 +351,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=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} 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"} 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} 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 +397,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=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"} 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."} 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 +436,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} 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() 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 tool_ctl.jumped_to_color = false
recolor(1) recolor(1)
end end
@ -471,10 +539,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
try_set(bundled, ini_cfg.EmerCoolColor ~= nil) try_set(bundled, ini_cfg.EmerCoolColor ~= nil)
if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end
try_set(invert, ini_cfg.EmerCoolInvert) try_set(invert, ini_cfg.EmerCoolInvert)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(svr_chan, ini_cfg.SVR_Channel) try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel) try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(timeout, ini_cfg.ConnTimeout) 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(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode) try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath) try_set(path, ini_cfg.LogPath)
@ -591,7 +662,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local val = util.strval(raw) local val = util.strval(raw)
if f[1] == "AuthKey" and raw then val = string.rep("*", string.len(val)) 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] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
elseif f[1] == "FrontPanelTheme" then elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw)) val = util.strval(themes.fp_theme_name(raw))
@ -601,7 +672,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if val == "nil" then val = "<not set>" end 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 alternate = not alternate
if (string.len(val) > val_max_w) or string.find(val, "\n") then if (string.len(val) > val_max_w) or string.find(val, "\n") then
@ -623,6 +694,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
end end
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 --#endregion
end end

View File

@ -3,6 +3,7 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -33,7 +34,8 @@ local changes = {
{ "v1.6.8", { "ConnTimeout can now have a fractional part" } }, { "v1.6.8", { "ConnTimeout can now have a fractional part" } },
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } }, { "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.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.9.1", { "Added support for wired communications modems" } }
} }
---@class plc_configurator ---@class plc_configurator
@ -68,6 +70,8 @@ local tool_ctl = {
gen_summary = nil, ---@type function gen_summary = nil, ---@type function
load_legacy = nil, ---@type function load_legacy = nil, ---@type function
gen_modem_list = function () end
} }
---@class plc_config ---@class plc_config
@ -78,6 +82,9 @@ local tmp_cfg = {
EmerCoolSide = nil, ---@type string|nil EmerCoolSide = nil, ---@type string|nil
EmerCoolColor = nil, ---@type color|nil EmerCoolColor = nil, ---@type color|nil
EmerCoolInvert = false, ---@type boolean EmerCoolInvert = false, ---@type boolean
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer PLC_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number ConnTimeout = nil, ---@type number
@ -103,6 +110,9 @@ local fields = {
{ "EmerCoolSide", "Emergency Coolant Side", nil }, { "EmerCoolSide", "Emergency Coolant Side", nil },
{ "EmerCoolColor", "Emergency Coolant Color", nil }, { "EmerCoolColor", "Emergency Coolant Color", nil },
{ "EmerCoolInvert", "Emergency Coolant Invert", false }, { "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 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 }, { "PLC_Channel", "PLC Channel", 16241 },
{ "ConnTimeout", "Connection Timeout", 5 }, { "ConnTimeout", "Connection Timeout", 5 },
@ -261,8 +271,13 @@ function configurator.configure(ask_config)
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg) tool_ctl.has_config = load_settings(ini_cfg)
-- set tmp_cfg so interface lists are correct
tmp_cfg.WiredModem = ini_cfg.WiredModem
reset_term() reset_term()
ppm.mount_all()
-- set overridden colors -- set overridden colors
for i = 1, #style.colors do for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex) 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} local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display) config_view(display)
tool_ctl.gen_modem_list()
while true do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
@ -288,6 +305,14 @@ function configurator.configure(ask_config)
display.handle_paste(param1) display.handle_paste(param1)
elseif event == "modem_message" then elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5) check.receive_sv(param1, param2, param3, param4, param5)
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 end
if event == "terminate" then return end if event == "terminate" then return end

View File

@ -53,7 +53,6 @@ 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("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("has_modem", not plc_state.no_modem)
databus.ps.publish("degraded", plc_state.degraded) databus.ps.publish("degraded", plc_state.degraded)
databus.ps.publish("init_ok", plc_state.init_ok)
end end
-- transmit thread (routine) statuses -- transmit thread (routine) statuses

View File

@ -51,11 +51,11 @@ local function init(panel)
local system = Div{parent=panel,width=14,height=18,x=2,y=3} local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local degraded = LED{parent=system,label="STATUS",colors=cpair(colors.red,colors.green)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
system.line_break() system.line_break()
init_ok.register(databus.ps, "init_ok", init_ok.update) degraded.register(databus.ps, "degraded", degraded.update)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}

View File

@ -48,6 +48,9 @@ function plc.load_config()
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel") config.PLC_Channel = settings.get("PLC_Channel")
config.ConnTimeout = settings.get("ConnTimeout") config.ConnTimeout = settings.get("ConnTimeout")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.TrustedRange = settings.get("TrustedRange") config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey") config.AuthKey = settings.get("AuthKey")
@ -70,11 +73,15 @@ function plc.validate_config(cfg)
cfv.assert_type_int(cfg.UnitID) cfv.assert_type_int(cfg.UnitID)
cfv.assert_type_bool(cfg.EmerCoolEnable) cfv.assert_type_bool(cfg.EmerCoolEnable)
if cfg.Networked == true then if cfg.Networked then
cfv.assert_channel(cfg.SVR_Channel) cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(cfg.PLC_Channel) cfv.assert_channel(cfg.PLC_Channel)
cfv.assert_type_num(cfg.ConnTimeout) cfv.assert_type_num(cfg.ConnTimeout)
cfv.assert_min(cfg.ConnTimeout, 2) cfv.assert_min(cfg.ConnTimeout, 2)
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_type_num(cfg.TrustedRange) cfv.assert_type_num(cfg.TrustedRange)
cfv.assert_min(cfg.TrustedRange, 0) cfv.assert_min(cfg.TrustedRange, 0)
cfv.assert_type_str(cfg.AuthKey) cfv.assert_type_str(cfg.AuthKey)
@ -118,7 +125,7 @@ function plc.rps_init(reactor, is_formed)
reactor_enabled = false, reactor_enabled = false,
enabled_at = 0, enabled_at = 0,
emer_cool_active = nil, ---@type boolean emer_cool_active = nil, ---@type boolean
formed = is_formed, formed = is_formed, ---@type boolean|nil
force_disabled = false, force_disabled = false,
tripped = false, tripped = false,
trip_cause = "ok" ---@type rps_trip_cause trip_cause = "ok" ---@type rps_trip_cause
@ -364,14 +371,16 @@ function plc.rps_init(reactor, is_formed)
return public.activate() return public.activate()
end end
-- check all safety conditions -- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions
---@nodiscard ---@nodiscard
---@param has_reactor boolean if the PLC state indicates we have a reactor
---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip ---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip
function public.check() function public.check(has_reactor)
local status = RPS_TRIP_CAUSE.OK local status = RPS_TRIP_CAUSE.OK
local was_tripped = self.tripped local was_tripped = self.tripped
local first_trip = false local first_trip = false
if has_reactor then
if self.formed then if self.formed then
-- update state -- update state
parallel.waitForAll( parallel.waitForAll(
@ -388,6 +397,10 @@ function plc.rps_init(reactor, is_formed)
-- check to see if its now formed -- check to see if its now formed
_is_formed() _is_formed()
end end
else
self.formed = nil
self.state[CHK.SYS_FAIL] = true
end
-- check system states in order of severity -- check system states in order of severity
if self.tripped then if self.tripped then
@ -474,6 +487,7 @@ function plc.rps_init(reactor, is_formed)
---@nodiscard ---@nodiscard
function public.is_active() return self.reactor_enabled end function public.is_active() return self.reactor_enabled end
---@nodiscard ---@nodiscard
---@return boolean|nil formed true if formed, false if not, nil if unknown
function public.is_formed() return self.formed end function public.is_formed() return self.formed end
---@nodiscard ---@nodiscard
function public.is_force_disabled() return self.force_disabled end function public.is_force_disabled() return self.force_disabled end
@ -495,14 +509,14 @@ function plc.rps_init(reactor, is_formed)
end end
-- partial RPS reset that only clears fault and sys_fail -- partial RPS reset that only clears fault and sys_fail
function public.reset_formed() function public.reset_reattach()
self.tripped = false self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK self.trip_cause = RPS_TRIP_CAUSE.OK
self.state[CHK.FAULT] = false self.state[CHK.FAULT] = false
self.state[CHK.SYS_FAIL] = false self.state[CHK.SYS_FAIL] = false
log.info("RPS: partial reset on formed") log.info("RPS: partial reset on connected or formed")
end end
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
@ -545,7 +559,9 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
max_burn_rate = nil max_burn_rate = nil
} }
if nic.isWireless() then
comms.set_trusted_range(config.TrustedRange) comms.set_trusted_range(config.TrustedRange)
end
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
@ -584,11 +600,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- dynamic reactor status information, excluding heating rate -- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted ---@return table data_table, boolean faulted
local function _get_reactor_status() local function _get_reactor_status()
local fuel = nil local fuel, waste, coolant, hcoolant = nil, nil, nil, nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {} local data_table = {}
reactor.__p_disable_afc() reactor.__p_disable_afc()
@ -707,11 +719,133 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
reactor.__p_enable_afc() reactor.__p_enable_afc()
end end
-- handle a burn rate command
---@param packet rplc_frame
---@param setpoints plc_setpoints
--- EVENT_CONSUMER: this function consumes events
local function _handle_burn_rate(packet, setpoints)
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
reactor.setBurnRate(burn_rate)
success = reactor.__p_is_ok()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
end
-- handle an auto burn rate command
---@param packet rplc_frame
---@param setpoints plc_setpoints
--- EVENT_CONSUMER: this function consumes events
local function _handle_auto_burn_rate(packet, setpoints)
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 100) / 100
local ramp = packet.data[2]
self.auto_ack_token = packet.data[3]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.01 then
if rps.is_active() then
-- auto scram to disable
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
else
log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
log.debug("AUTO: activating the reactor")
reactor.setBurnRate(0.01)
if reactor.__p_is_faulted() then
log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
log.warning("AUTO: automatic reactor activation failed")
end
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
reactor.setBurnRate(burn_rate)
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class plc_comms ---@class plc_comms
local public = {} local public = {}
-- switch the current active NIC
---@param _nic nic
function public.switch_nic(_nic)
nic.closeAll()
if _nic.isWireless() then
comms.set_trusted_range(config.TrustedRange)
end
-- configure receive channels
_nic.closeAll()
_nic.open(config.PLC_Channel)
nic = _nic
end
-- reconnect a newly connected reactor -- reconnect a newly connected reactor
---@param new_reactor table ---@param new_reactor table
function public.reconnect_reactor(new_reactor) function public.reconnect_reactor(new_reactor)
@ -803,15 +937,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- get as RPLC packet -- get as RPLC packet
if s_pkt.protocol() == PROTOCOL.RPLC then if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end
pkt = rplc_pkt.get()
end
-- get as SCADA management packet -- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end
pkt = mgmt_pkt.get()
end
else else
log.debug("unsupported packet type " .. s_pkt.protocol(), true) log.debug("unsupported packet type " .. s_pkt.protocol(), true)
end end
@ -823,16 +953,13 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- handle RPLC and MGMT packets -- handle RPLC and MGMT packets
---@param packet rplc_frame|mgmt_frame packet frame ---@param packet rplc_frame|mgmt_frame packet frame
---@param plc_state plc_state PLC state ---@param plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table ---@param setpoints plc_setpoints setpoint control table
function public.handle_packet(packet, plc_state, setpoints) ---@param println_ts function console print, when UI isn't running
-- print a log message to the terminal as long as the UI isn't running function public.handle_packet(packet, plc_state, setpoints, println_ts)
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
-- handle packets now that we have prints setup
if l_chan == config.PLC_Channel then if l_chan == config.PLC_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
@ -867,36 +994,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
log.debug("sent out structure again, did supervisor miss it?") log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
-- set the burn rate -- set the burn rate
if (packet.length == 2) and (type(packet.data[1]) == "number") then _handle_burn_rate(packet, setpoints)
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
reactor.setBurnRate(burn_rate)
success = reactor.__p_is_ok()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
elseif packet.type == RPLC_TYPE.RPS_ENABLE then elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor -- enable the reactor
self.scrammed = false self.scrammed = false
@ -925,68 +1023,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send_ack(packet.type, true) _send_ack(packet.type, true)
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
-- automatic control requested a new burn rate -- automatic control requested a new burn rate
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then _handle_auto_burn_rate(packet, setpoints)
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 100) / 100
local ramp = packet.data[2]
self.auto_ack_token = packet.data[3]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.01 then
if rps.is_active() then
-- auto scram to disable
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
else
log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
log.debug("AUTO: activating the reactor")
reactor.setBurnRate(0.01)
if reactor.__p_is_faulted() then
log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
log.warning("AUTO: automatic reactor activation failed")
end
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
reactor.setBurnRate(burn_rate)
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
else else
log.debug("received unknown RPLC packet type " .. packet.type) log.debug("received unknown RPLC packet type " .. packet.type)
end end

View File

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

View File

@ -3,6 +3,7 @@
-- --
require("/initenv").init_env() require("/initenv").init_env()
local backplane = require("reactor-plc.backplane")
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
@ -18,7 +19,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.8.22" local R_PLC_VERSION = "v1.9.1"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -87,29 +88,30 @@ local function main()
-- PLC system state flags -- PLC system state flags
---@class plc_state ---@class plc_state
plc_state = { plc_state = {
init_ok = true,
fp_ok = false, fp_ok = false,
shutdown = false, shutdown = false,
degraded = true, degraded = true,
reactor_formed = true,
no_reactor = true, no_reactor = true,
no_modem = true reactor_formed = true,
wd_modem = false,
wl_modem = false
}, },
-- control setpoints -- control setpoints
---@class setpoints ---@class plc_setpoints
setpoints = { setpoints = {
burn_rate_en = false, burn_rate_en = false,
burn_rate = 0.0 burn_rate = 0.0
}, },
-- core PLC devices -- global PLC devices, still initialized by the backplane
---@class plc_dev
plc_dev = { plc_dev = {
reactor = ppm.get_fission_reactor(), reactor = nil ---@type table
modem = ppm.get_wireless_modem()
}, },
-- system objects -- system objects
---@class plc_sys
plc_sys = { plc_sys = {
rps = nil, ---@type rps rps = nil, ---@type rps
nic = nil, ---@type nic nic = nil, ---@type nic
@ -122,6 +124,18 @@ local function main()
mq_rps = mqueue.new(), mq_rps = mqueue.new(),
mq_comms_tx = mqueue.new(), mq_comms_tx = mqueue.new(),
mq_comms_rx = mqueue.new() mq_comms_rx = mqueue.new()
},
-- message queue commands
q_cmds = {
MQ__RPS_CMD = {
SCRAM = 1,
DEGRADED_SCRAM = 2,
TRIP_TIMEOUT = 3
},
MQ__COMM_CMD = {
SEND_STATUS = 1
}
} }
} }
@ -130,110 +144,66 @@ local function main()
local plc_state = __shared_memory.plc_state local plc_state = __shared_memory.plc_state
-- initial state evaluation -- reactor and modem initialization
plc_state.no_reactor = smem_dev.reactor == nil backplane.init(config, __shared_memory)
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("init> fission reactor not found")
log.warning("init> no reactor on startup")
plc_state.init_ok = false
plc_state.degraded = true
elseif not smem_dev.reactor.isFormed() then
println("init> fission reactor is not formed")
log.warning("init> 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("init> wireless modem not found")
log.warning("init> 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.init_ok = false
plc_state.degraded = true
end
-- print a log message to the terminal as long as the UI isn't running
local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
-- PLC init<br>
--- EVENT_CONSUMER: this function consumes events
local function init()
-- scram on boot if networked, otherwise leave the reactor be -- 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 if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
log.debug("startup> power-on SCRAM")
smem_dev.reactor.scram() smem_dev.reactor.scram()
end end
-- setup front panel -- setup front panel
if not renderer.ui_ready() then
local message 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 -- ...or not
if not plc_state.fp_ok then if not plc_state.fp_ok then
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message)) log.error(util.c("front panel GUI render failed with error ", message))
log.info("init> running in headless mode without front panel") log.info("startup> running in headless mode without front panel")
end
end end
if plc_state.init_ok then -- print a log message to the terminal as long as the UI isn't running
local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) end end
----------------------------------------
-- initialize PLC
----------------------------------------
-- init reactor protection system -- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) smem_sys.rps = plc.rps_init(smem_dev.reactor, util.trinary(plc_state.no_reactor, nil, plc_state.reactor_formed))
log.debug("init> rps init") log.debug("startup> rps init")
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("init> 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)
log.debug("init> comms init")
else
_println_no_fp("init> starting in offline mode")
log.info("init> running without networking")
end
-- notify user of emergency coolant configuration status -- notify user of emergency coolant configuration status
if config.EmerCoolEnable then if config.EmerCoolEnable then
println("init> emergency coolant control ready") _println_no_fp("startup> emergency coolant control ready")
log.info("init> running with emergency coolant control available") log.info("startup> emergency coolant control available")
end end
util.push_event("clock_start") -- conditionally init comms
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
_println_no_fp("init> completed") -- create network interface then setup comms
log.info("init> startup completed") smem_sys.nic = backplane.active_nic()
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("startup> comms init")
else else
_println_no_fp("init> system in degraded state, awaiting devices...") _println_no_fp("startup> starting in non-networked mode")
log.warning("init> started in a degraded state, awaiting peripheral connections...") log.info("startup> starting without networking")
end end
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
end
---------------------------------------- _println_no_fp("startup> completed")
-- start system log.info("startup> completed")
----------------------------------------
-- initialize PLC
init()
-- init threads -- init threads
local main_thread = threads.thread__main(__shared_memory, init) local main_thread = threads.thread__main(__shared_memory)
local rps_thread = threads.thread__rps(__shared_memory) local rps_thread = threads.thread__rps(__shared_memory)
if __shared_memory.networked then if __shared_memory.networked then
@ -247,14 +217,12 @@ local function main()
-- run threads -- run threads
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec) parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec)
if plc_state.init_ok then
-- send status one last time after RPS shutdown -- send status one last time after RPS shutdown
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
smem_sys.plc_comms.send_rps_status() smem_sys.plc_comms.send_rps_status()
-- close connection -- close connection
smem_sys.plc_comms.close() smem_sys.plc_comms.close()
end
else else
-- run threads, excluding comms -- run threads, excluding comms
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)

View File

@ -4,6 +4,7 @@ local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local backplane = require("reactor-plc.backplane")
local databus = require("reactor-plc.databus") local databus = require("reactor-plc.databus")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
@ -11,28 +12,17 @@ local core = require("graphics.core")
local threads = {} local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local RPS_SLEEP = 250 -- (250ms, 5 ticks) local RPS_SLEEP = 250 -- 250ms, 5 ticks
local COMMS_SLEEP = 150 -- (150ms, 3 ticks) local COMMS_SLEEP = 150 -- 150ms, 3 ticks
local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks) local SP_CTRL_SLEEP = 250 -- 250ms, 5 ticks
local BURN_RATE_RAMP_mB_s = 5.0 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 -- main thread
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
---@param init function function threads.thread__main(smem)
function threads.thread__main(smem, init)
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
@ -42,7 +32,7 @@ function threads.thread__main(smem, init)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("main", true) databus.tx_rt_status("main", true)
log.debug("main thread init, clock inactive") log.debug("OS: main thread start")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- 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) -- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks)
@ -55,6 +45,12 @@ function threads.thread__main(smem, init)
local plc_state = smem.plc_state local plc_state = smem.plc_state
local plc_dev = smem.plc_dev local plc_dev = smem.plc_dev
local MQ__RPS_CMD = smem.q_cmds.MQ__RPS_CMD
local MQ__COMM_CMD = smem.q_cmds.MQ__COMM_CMD
-- start clock
loop_clock.start()
-- event loop -- event loop
while true do while true do
-- get plc_sys fields (may have been set late due to degraded boot) -- get plc_sys fields (may have been set late due to degraded boot)
@ -67,7 +63,6 @@ function threads.thread__main(smem, init)
-- handle event -- handle event
if event == "timer" and loop_clock.is_clock(param1) then if event == "timer" and loop_clock.is_clock(param1) then
-- note: loop clock is only running if init_ok = true
-- blink heartbeat indicator -- blink heartbeat indicator
databus.heartbeat() databus.heartbeat()
@ -93,7 +88,7 @@ function threads.thread__main(smem, init)
-- reactor now formed -- reactor now formed
plc_state.reactor_formed = true plc_state.reactor_formed = true
println_ts("reactor is now formed.") println_ts("reactor is now formed")
log.info("reactor is now formed") log.info("reactor is now formed")
-- SCRAM newly formed reactor -- SCRAM newly formed reactor
@ -106,10 +101,10 @@ function threads.thread__main(smem, init)
-- partial reset of RPS, specific to becoming formed -- partial reset of RPS, specific to becoming formed
-- without this, auto control can't resume on chunk load -- without this, auto control can't resume on chunk load
rps.reset_formed() rps.reset_reattach()
elseif plc_state.reactor_formed and not rps.is_formed() then elseif plc_state.reactor_formed and (rps.is_formed() == false) then
-- reactor no longer formed -- reactor no longer formed
println_ts("reactor is no longer formed.") println_ts("reactor is no longer formed")
log.info("reactor is no longer formed") log.info("reactor is no longer formed")
plc_state.reactor_formed = false plc_state.reactor_formed = false
@ -118,14 +113,14 @@ function threads.thread__main(smem, init)
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then elseif event == "modem_message" and networked and nic.is_connected() then
-- got a packet -- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then if packet ~= nil then
-- pass the packet onto the comms message queue -- pass the packet onto the comms message queue
smem.q.mq_comms_rx.push_packet(packet) smem.q.mq_comms_rx.push_packet(packet)
end end
elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then
-- haven't heard from server recently? close connection and shutdown reactor -- haven't heard from server recently? close connection and shutdown reactor
plc_comms.close() plc_comms.close()
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
@ -145,29 +140,26 @@ function threads.thread__main(smem, init)
plc_state.degraded = true plc_state.degraded = true
elseif networked and type == "modem" then elseif networked and type == "modem" then
---@cast device Modem ---@cast device Modem
-- we only care if this is our wireless modem -- we only care if this is our comms modem
-- note, check init_ok first since nic will be nil if it is false if nic.is_modem(device) then
if plc_state.init_ok and nic.is_modem(device) then
nic.disconnect() nic.disconnect()
println_ts("comms modem disconnected!") println_ts("comms modem disconnected!")
log.warning("comms modem disconnected") log.warning("comms modem disconnected")
local other_modem = ppm.get_wireless_modem() local other_modem = ppm.get_wireless_modem()
if other_modem then if other_modem and not plc_dev.modem_wired then
log.info("found another wireless modem, using it for comms") log.info("found another wireless modem, using it for comms")
nic.connect(other_modem) nic.connect(other_modem)
else else
plc_state.no_modem = true plc_state.no_modem = true
plc_state.degraded = true plc_state.degraded = true
if plc_state.init_ok then
-- try to scram reactor if it is still connected -- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end end
end
else else
log.warning("a modem was disconnected") log.warning("non-comms modem disconnected")
end end
end end
end end
@ -177,66 +169,8 @@ function threads.thread__main(smem, init)
elseif event == "peripheral" then elseif event == "peripheral" then
-- peripheral connect -- peripheral connect
local type, device = ppm.mount(param1) local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if plc_state.no_reactor and (type == "fissionReactorLogicAdapter") then backplane.attach(param1, type, device, println_ts)
-- 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
if plc_state.init_ok then
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_formed()
end
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 (plc_state.init_ok and nic.is_connected()) then
-- reconnected modem
plc_dev.modem = device
plc_state.no_modem = false
if plc_state.init_ok then nic.connect(device) end
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
-- determine if we are still in a degraded state
if not plc_state.no_reactor then
plc_state.degraded = false
end
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else
log.info("wired modem reconnected")
end
end
end
-- if not init'd and no longer degraded, proceed to init
if not plc_state.init_ok and not plc_state.degraded then
plc_state.init_ok = true
init()
end end
-- update indicators -- update indicators
@ -245,15 +179,11 @@ function threads.thread__main(smem, init)
event == "double_click" then event == "double_click" then
-- handle a mouse event -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "clock_start" then
-- start loop clock
loop_clock.start()
log.debug("main thread clock started")
end end
-- check for termination request -- check for termination request
if event == "terminate" or ppm.should_terminate() then if event == "terminate" or ppm.should_terminate() then
log.info("terminate requested, main thread exiting") log.info("OS: terminate requested, main thread exiting")
-- rps handles reactor shutdown -- rps handles reactor shutdown
plc_state.shutdown = true plc_state.shutdown = true
break break
@ -277,8 +207,7 @@ function threads.thread__main(smem, init)
-- if not, we need to restart the clock -- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("main thread restarting now...") log.info("OS: main thread restarting now...")
util.push_event("clock_start")
end end
end end
end end
@ -299,7 +228,7 @@ function threads.thread__rps(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("rps", true) databus.tx_rt_status("rps", true)
log.debug("rps thread start") log.debug("OS: rps thread start")
-- load in from shared memory -- load in from shared memory
local networked = smem.networked local networked = smem.networked
@ -308,6 +237,8 @@ function threads.thread__rps(smem)
local rps_queue = smem.q.mq_rps local rps_queue = smem.q.mq_rps
local MQ__RPS_CMD = smem.q_cmds.MQ__RPS_CMD
local was_linked = false local was_linked = false
local last_update = util.time() local last_update = util.time()
@ -316,32 +247,25 @@ function threads.thread__rps(smem)
-- get plc_sys fields (may have been set late due to degraded boot) -- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps local rps = smem.plc_sys.rps
local plc_comms = smem.plc_sys.plc_comms local plc_comms = smem.plc_sys.plc_comms
-- get reactor, may have changed do to disconnect/reconnect -- get reactor, it may have changed due to a disconnect/reconnect
local reactor = plc_dev.reactor local reactor = plc_dev.reactor
-- RPS checks
if plc_state.init_ok then
-- SCRAM if no open connection -- SCRAM if no open connection
if networked and not plc_comms.is_linked() then if networked and not plc_comms.is_linked() then
if was_linked then if was_linked then
was_linked = false was_linked = false
rps.trip_timeout() rps.trip_timeout()
end end
else else was_linked = true end
was_linked = true
end
if (not plc_state.no_reactor) and rps.is_formed() then
-- check reactor status -- check reactor status
---@diagnostic disable-next-line: need-check-nil if (not plc_state.no_reactor) and rps.is_formed() then
local reactor_status = reactor.getStatus() local reactor_status = reactor.getStatus()
databus.tx_reactor_state(reactor_status) databus.tx_reactor_state(reactor_status)
-- if we tried to SCRAM but failed, keep trying -- if we tried to SCRAM but failed, keep trying
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
if rps.is_tripped() and reactor_status then if rps.is_tripped() and reactor_status then rps.scram() end
rps.scram()
end
end end
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS -- if we are in standalone mode and the front panel isn't working, continuously reset RPS
@ -349,16 +273,10 @@ function threads.thread__rps(smem)
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
-- check safety (SCRAM occurs if tripped) -- check safety (SCRAM occurs if tripped)
if not plc_state.no_reactor then local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor)
local rps_tripped, rps_status_string, rps_first = rps.check()
if rps_tripped and rps_first then if rps_tripped and rps_first then
println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")")
if networked and not plc_state.no_modem then if networked then plc_comms.send_rps_alarm(rps_status_string) end
plc_comms.send_rps_alarm(rps_status_string)
end
end
end
end end
-- check for messages in the message queue -- check for messages in the message queue
@ -368,19 +286,19 @@ function threads.thread__rps(smem)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
if plc_state.init_ok then
if msg.message == MQ__RPS_CMD.SCRAM then if msg.message == MQ__RPS_CMD.SCRAM then
-- SCRAM -- SCRAM
log.info("RPS: OS requested SCRAM")
rps.scram() rps.scram()
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
-- lost peripheral(s) -- lost peripheral(s)
log.info("RPS: received PLC degraded alert")
rps.trip_fault() rps.trip_fault()
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
-- watchdog tripped -- watchdog tripped
println_ts("RPS: supervisor timeout")
log.warning("RPS: received supervisor timeout alert")
rps.trip_timeout() rps.trip_timeout()
println_ts("server timeout")
log.warning("server timeout")
end
end end
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
-- received data -- received data
@ -396,17 +314,17 @@ function threads.thread__rps(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
-- safe exit -- safe exit
log.info("rps thread shutdown initiated") log.info("OS: rps thread shutdown initiated")
if plc_state.init_ok then
if rps.scram() then if rps.scram() then
println_ts("reactor disabled") println_ts("exiting, reactor disabled")
log.info("rps thread reactor SCRAM OK") log.info("OS: rps thread reactor SCRAM OK on exit")
else else
println_ts("exiting, reactor failed to disable") println_ts("exiting, reactor failed to disable")
log.error("rps thread failed to SCRAM reactor on exit") log.error("OS: rps thread failed to SCRAM reactor on exit")
end end
end
log.info("rps thread exiting") log.info("OS: rps thread exiting")
break break
end end
@ -428,8 +346,8 @@ function threads.thread__rps(smem)
databus.tx_rt_status("rps", false) databus.tx_rt_status("rps", false)
if not plc_state.shutdown then if not plc_state.shutdown then
if plc_state.init_ok then smem.plc_sys.rps.scram() end smem.plc_sys.rps.scram()
log.info("rps thread restarting in 5 seconds...") log.info("OS: rps thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -448,12 +366,14 @@ function threads.thread__comms_tx(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_tx", true) databus.tx_rt_status("comms_tx", true)
log.debug("comms tx thread start") log.debug("OS: comms tx thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state local plc_state = smem.plc_state
local comms_queue = smem.q.mq_comms_tx local comms_queue = smem.q.mq_comms_tx
local MQ__COMM_CMD = smem.q_cmds.MQ__COMM_CMD
local last_update = util.time() local last_update = util.time()
-- thread loop -- thread loop
@ -465,7 +385,7 @@ function threads.thread__comms_tx(smem)
while comms_queue.ready() and not plc_state.shutdown do while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop() local msg = comms_queue.pop()
if msg ~= nil and plc_state.init_ok then if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
if msg.message == MQ__COMM_CMD.SEND_STATUS then if msg.message == MQ__COMM_CMD.SEND_STATUS then
@ -486,7 +406,7 @@ function threads.thread__comms_tx(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("comms tx thread exiting") log.info("OS: comms tx thread exiting")
break break
end end
@ -508,7 +428,7 @@ function threads.thread__comms_tx(smem)
databus.tx_rt_status("comms_tx", false) databus.tx_rt_status("comms_tx", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("comms tx thread restarting in 5 seconds...") log.info("OS: comms tx thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -521,13 +441,16 @@ end
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__comms_rx(smem) function threads.thread__comms_rx(smem)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
---@class parallel_thread ---@class parallel_thread
local public = {} local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_rx", true) databus.tx_rt_status("comms_rx", true)
log.debug("comms rx thread start") log.debug("OS: comms rx thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state local plc_state = smem.plc_state
@ -546,7 +469,7 @@ function threads.thread__comms_rx(smem)
while comms_queue.ready() and not plc_state.shutdown do while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop() local msg = comms_queue.pop()
if msg ~= nil and plc_state.init_ok then if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@ -555,7 +478,7 @@ function threads.thread__comms_rx(smem)
-- received a packet -- received a packet
-- handle the packet (setpoints passed to update burn rate setpoint) -- handle the packet (setpoints passed to update burn rate setpoint)
-- (plc_state passed to check if degraded) -- (plc_state passed to check if degraded)
plc_comms.handle_packet(msg.message, plc_state, setpoints) plc_comms.handle_packet(msg.message, plc_state, setpoints, println_ts)
end end
end end
@ -565,7 +488,7 @@ function threads.thread__comms_rx(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("comms rx thread exiting") log.info("OS: comms rx thread exiting")
break break
end end
@ -587,7 +510,7 @@ function threads.thread__comms_rx(smem)
databus.tx_rt_status("comms_rx", false) databus.tx_rt_status("comms_rx", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("comms rx thread restarting in 5 seconds...") log.info("OS: comms rx thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -606,7 +529,7 @@ function threads.thread__setpoint_control(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("spctl", true) databus.tx_rt_status("spctl", true)
log.debug("setpoint control thread start") log.debug("OS: setpoint control thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state local plc_state = smem.plc_state
@ -629,9 +552,7 @@ function threads.thread__setpoint_control(smem)
-- get reactor, may have changed do to disconnect/reconnect -- get reactor, may have changed do to disconnect/reconnect
local reactor = plc_dev.reactor local reactor = plc_dev.reactor
if plc_state.init_ok and (not plc_state.no_reactor) then if not plc_state.no_reactor then
---@cast reactor table won't be nil
-- check if we should start ramping -- check if we should start ramping
if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then
local cur_burn_rate = reactor.getBurnRate() local cur_burn_rate = reactor.getBurnRate()
@ -698,7 +619,7 @@ function threads.thread__setpoint_control(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("setpoint control thread exiting") log.info("OS: setpoint control thread exiting")
break break
end end
@ -720,7 +641,7 @@ function threads.thread__setpoint_control(smem)
databus.tx_rt_status("spctl", false) databus.tx_rt_status("spctl", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("setpoint control thread restarting in 5 seconds...") log.info("OS: setpoint control thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end

274
rtu/backplane.lua Normal file
View File

@ -0,0 +1,274 @@
--
-- 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")
---@class rtu_backplane
local backplane = {}
local _bp = {
smem = nil, ---@type rtu_shared_memory
wlan_pref = true,
lan_iface = "",
act_nic = nil, ---@type nic|nil
wl_act = true,
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
function backplane.init(config, __shared_memory)
_bp.smem = __shared_memory
_bp.wlan_pref = config.PreferWireless
_bp.lan_iface = config.WiredModem
-- init wired NIC
if type(config.WiredModem) == "string" then
local modem = ppm.get_modem(_bp.lan_iface)
if modem then
_bp.wd_nic = network.nic(modem)
log.info("BKPLN: WIRED PHY_UP " .. _bp.lan_iface)
end
end
-- init wireless NIC(s)
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
if modem then
_bp.wl_nic = network.nic(modem)
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
end
end
-- grab the preferred active NIC
if _bp.wlan_pref then
_bp.wl_act = true
_bp.act_nic = _bp.wl_nic
else
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
end
databus.tx_hw_modem(_bp.act_nic ~= nil)
-- 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(_bp.sounders, sounder)
log.debug(util.c("BKPLN: added speaker, attached as ", sounder.name))
end
databus.tx_hw_spkr_count(#_bp.sounders)
end
-- get the active NIC
---@return nic|nil
function backplane.active_nic() return _bp.act_nic end
-- get the sounder interfaces
---@return rtu_speaker_sounder[]
function backplane.sounders() return _bp.sounders end
-- handle a backplane peripheral detach
---@param type string
---@param device table
---@param iface string
function backplane.detach(type, device, iface)
local function println_ts(message) if not _bp.smem.rtu_state.fp_ok then util.println_ts(message) end end
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()
local was_active = _bp.act_nic and _bp.act_nic.is_modem(device)
local was_wd = wd_nic and wd_nic.is_modem(device)
local was_wl = wl_nic and wl_nic.is_modem(device)
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_DETACH ", iface))
if wd_nic and was_wd then
wd_nic.disconnect()
log.info("BKPLN: WIRED PHY_DOWN " .. iface)
elseif wl_nic and was_wl then
wl_nic.disconnect()
log.info("BKPLN: WIRELESS PHY_DOWN " .. iface)
end
-- we only care if this is our active comms modem
if was_active then
println_ts("active comms modem disconnected")
log.warning("BKPLN: active comms modem disconnected")
-- failover and try to find a new comms modem
if _bp.wl_act then
-- try to find another wireless modem, otherwise switch to wired
local modem, m_iface = ppm.get_wireless_modem()
if modem then
log.info("BKPLN: found another wireless modem, using it for comms")
-- note: must assign to self.wl_nic if creating a nic, otherwise it only changes locally
if wl_nic then
wl_nic.connect(modem)
else _bp.wl_nic = network.nic(modem) end
log.info("BKPLN: WIRELESS PHY_UP " .. m_iface)
_bp.act_nic = wl_nic
comms.assign_nic(_bp.act_nic)
log.info("BKPLN: switched comms to new wireless modem")
elseif wd_nic and wd_nic.is_connected() then
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
comms.assign_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem")
else
_bp.act_nic = nil
databus.tx_hw_modem(false)
comms.unassign_nic()
end
else
-- switch to wireless if able
if wl_nic then
_bp.wl_act = true
_bp.act_nic = wl_nic
comms.assign_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem")
else
_bp.act_nic = nil
databus.tx_hw_modem(false)
comms.unassign_nic()
end
end
elseif _bp.wl_nic and m_is_wl then
-- wireless, but not active
log.info("BKPLN: standby wireless modem disconnected")
else
log.warning("BKPLN: unassigned modem disconnected")
end
elseif type == "speaker" then
---@cast device Speaker
for i = 1, #_bp.sounders do
if _bp.sounders[i].speaker == device then
table.remove(_bp.sounders, i)
log.warning(util.c("BKPLN: speaker ", iface, " disconnected"))
println_ts("speaker disconnected")
databus.tx_hw_spkr_count(#_bp.sounders)
break
end
end
end
end
-- handle a backplane peripheral attach
---@param type string
---@param device table
---@param iface string
function backplane.attach(type, device, iface)
local function println_ts(message) if not _bp.smem.rtu_state.fp_ok then util.println_ts(message) end end
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))
local is_wd = _bp.lan_iface == iface
local is_wl = ((not _bp.wl_nic) or (not _bp.wl_nic.is_connected())) and m_is_wl
if is_wd then
-- connect this as the wired NIC
if _bp.wd_nic then
_bp.wd_nic.connect(device)
else _bp.wd_nic = network.nic(device) end
log.info("BKPLN: WIRED PHY_UP " .. iface)
if _bp.act_nic == nil then
-- set as active
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
comms.assign_nic(_bp.act_nic)
databus.tx_hw_modem(true)
println_ts("comms modem reconnected")
log.info("BKPLN: switched comms to wired modem")
elseif _bp.wl_act and not _bp.wlan_pref then
-- switch back to preferred wired
_bp.wl_act = false
_bp.act_nic = _bp.wd_nic
comms.assign_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wired modem (preferred)")
end
elseif is_wl then
-- connect this as the wireless NIC
if _bp.wl_nic then
_bp.wl_nic.connect(device)
else _bp.wl_nic = network.nic(device) end
log.info("BKPLN: WIRELESS PHY_UP " .. iface)
if _bp.act_nic == nil then
-- set as active
_bp.wl_act = true
_bp.act_nic = _bp.wl_nic
comms.assign_nic(_bp.act_nic)
databus.tx_hw_modem(true)
println_ts("comms modem reconnected")
log.info("BKPLN: switched comms to wireless modem")
elseif (not _bp.wl_act) and _bp.wlan_pref then
-- switch back to preferred wireless
_bp.wl_act = true
_bp.act_nic = _bp.wl_nic
comms.assign_nic(_bp.act_nic)
log.info("BKPLN: switched comms to wireless modem (preferred)")
end
elseif m_is_wl then
-- the wireless NIC already has a modem
log.info("standby wireless modem connected")
else
log.info("wired modem connected")
end
elseif type == "speaker" then
---@cast device Speaker
table.insert(_bp.sounders, rtu.init_sounder(device))
println_ts("speaker connected")
log.info(util.c("connected speaker ", iface))
databus.tx_hw_spkr_count(#_bp.sounders)
end
end
return backplane

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} 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"} 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)} 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} 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,8 @@ local self = {
importing_legacy = false, importing_legacy = false,
importing_any_dc = false, importing_any_dc = false,
wl_pref = nil, ---@type Checkbox
range = nil, ---@type NumberField
show_auth_key = nil, ---@type function show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox auth_key_textbox = nil, ---@type TextBox
@ -90,22 +92,77 @@ 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_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_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_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_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=1,text="Please select the network interface(s)."}
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=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 function dis_pref(value)
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} if not value then
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} self.wl_pref.set_value(false)
TextBox{parent=net_c_1,x=1,y=11,text="RTU Channel"} self.wl_pref.disable()
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} else self.wl_pref.enable() end
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[RTU_CHANNEL]",fg_bg=g_lg_fg_bg} 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(_) tool_ctl.gen_modem_list() end
local wireless = Checkbox{parent=net_c_1,x=1,y=3,label="Wireless/Ender Modem",default=ini_cfg.WirelessModem,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=dis_pref}
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}
local wired = Checkbox{parent=net_c_1,x=1,y=5,label="Wired Modem",default=ini_cfg.WiredModem~=false,box_fg_bg=cpair(colors.lightBlue,colors.black),callback=on_wired_change}
TextBox{parent=net_c_1,x=3,y=6,text="MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting to peripherals will cause problems",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local modem_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
dis_pref(ini_cfg.WirelessModem)
local function submit_interfaces()
tmp_cfg.WirelessModem = wireless.get_value()
tmp_cfg.PreferWireless = tmp_cfg.WirelessModem and self.wl_pref.get_value()
if not wired.get_value() then
tmp_cfg.WiredModem = false
tool_ctl.gen_modem_list()
end
if not (wired.get_value() or wireless.get_value()) then
modem_err.set_value("Please select a modem type.")
modem_err.show()
elseif wired.get_value() and type(tmp_cfg.WiredModem) ~= "string" then
modem_err.set_value("Please select a wired modem.")
modem_err.show()
else
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 function submit_channels()
local svr_c = tonumber(svr_chan.get_value()) local svr_c = tonumber(svr_chan.get_value())
@ -113,7 +170,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
if svr_c ~= nil and rtu_c ~= nil then if svr_c ~= nil and rtu_c ~= nil then
tmp_cfg.SVR_Channel = svr_c tmp_cfg.SVR_Channel = svr_c
tmp_cfg.RTU_Channel = rtu_c tmp_cfg.RTU_Channel = rtu_c
net_pane.set_value(2) net_pane.set_value(3)
chan_err.hide(true) chan_err.hide(true)
elseif svr_c == nil then elseif svr_c == nil then
chan_err.set_value("Please set the supervisor channel.") chan_err.set_value("Please set the supervisor channel.")
@ -124,54 +181,62 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end end
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_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_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=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"} TextBox{parent=net_c_3,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} 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_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_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_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=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"} TextBox{parent=net_c_3,x=1,y=8,text="Trusted Range (Wireless Only)"}
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} 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_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=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 function submit_ct_tr()
local timeout_val = tonumber(timeout.get_value()) local timeout_val = tonumber(timeout.get_value())
local range_val = tonumber(range.get_value()) local range_val = tonumber(self.range.get_value())
if timeout_val ~= nil and range_val ~= nil then
tmp_cfg.ConnTimeout = timeout_val if timeout_val == nil then
tmp_cfg.TrustedRange = range_val n3_err.set_value("Please set the connection timeout.")
net_pane.set_value(3) n3_err.show()
p2_err.hide(true) elseif tmp_cfg.WirelessModem and (range_val == nil) then
elseif timeout_val == nil then n3_err.set_value("Please set the trusted range.")
p2_err.set_value("Please set the connection timeout.") n3_err.show()
p2_err.show()
else else
p2_err.set_value("Please set the trusted range.") tmp_cfg.ConnTimeout = timeout_val
p2_err.show() 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
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_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_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=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_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_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=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"} TextBox{parent=net_c_4,x=1,y=11,text="Auth Key (Wireless Only, Not Used for Wired)"}
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} 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 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) hide_key.set_value(true)
censor_key(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 function submit_auth()
local v = key.get_value() local v = key.get_value()
@ -182,8 +247,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else key_err.show() end else key_err.show() end
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_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_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=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
@ -196,7 +261,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=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} 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"} 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} 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 +303,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=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"} 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."} 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 +447,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
load_settings(ini_cfg) load_settings(ini_cfg)
try_set(s_vol, ini_cfg.SpeakerVolume) try_set(s_vol, ini_cfg.SpeakerVolume)
try_set(wireless, ini_cfg.WirelessModem)
try_set(wired, ini_cfg.WiredModem ~= false)
try_set(self.wl_pref, ini_cfg.PreferWireless)
try_set(svr_chan, ini_cfg.SVR_Channel) try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(rtu_chan, ini_cfg.RTU_Channel) try_set(rtu_chan, ini_cfg.RTU_Channel)
try_set(timeout, ini_cfg.ConnTimeout) 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(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode) try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath) try_set(path, ini_cfg.LogPath)
@ -665,6 +733,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
end end
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 --#endregion
end 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.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.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } },
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } }, { "v1.11.8", { "Added advanced option to invert digital redstone signals" } },
{ "v1.12.0", { "Added support for redstone relays" } } { "v1.12.0", { "Added support for redstone relays" } },
{ "v1.13.0", { "Added support for wired communications modems" } }
} }
---@class rtu_configurator ---@class rtu_configurator
@ -80,6 +81,8 @@ local tool_ctl = {
update_relay_list = nil, ---@type function update_relay_list = nil, ---@type function
gen_peri_summary = nil, ---@type function gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function gen_rs_summary = nil, ---@type function
gen_modem_list = function () end
} }
---@class rtu_config ---@class rtu_config
@ -87,11 +90,14 @@ local tmp_cfg = {
SpeakerVolume = 1.0, SpeakerVolume = 1.0,
Peripherals = {}, ---@type rtu_peri_definition[] Peripherals = {}, ---@type rtu_peri_definition[]
Redstone = {}, ---@type rtu_rs_definition[] Redstone = {}, ---@type rtu_rs_definition[]
WirelessModem = true,
WiredModem = false, ---@type string|false
PreferWireless = true,
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer RTU_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number ConnTimeout = nil, ---@type number
TrustedRange = nil, ---@type number TrustedRange = nil, ---@type number
AuthKey = nil, ---@type string|nil AuthKey = nil, ---@type string
LogMode = 0, ---@type LOG_MODE LogMode = 0, ---@type LOG_MODE
LogPath = "", LogPath = "",
LogDebug = false, LogDebug = false,
@ -106,6 +112,9 @@ local settings_cfg = {}
local fields = { local fields = {
{ "SpeakerVolume", "Speaker Volume", 1.0 }, { "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 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "RTU_Channel", "RTU Channel", 16242 }, { "RTU_Channel", "RTU Channel", 16242 },
{ "ConnTimeout", "Connection Timeout", 5 }, { "ConnTimeout", "Connection Timeout", 5 },
@ -313,6 +322,9 @@ function configurator.configure(ask_config)
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
tool_ctl.has_config = load_settings(ini_cfg) 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.Peripherals = tool_ctl.deep_copy_peri(ini_cfg.Peripherals)
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) 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} local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display) config_view(display)
tool_ctl.gen_modem_list()
while true do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
@ -350,11 +364,13 @@ function configurator.configure(ask_config)
ppm.handle_unmount(param1) ppm.handle_unmount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
tool_ctl.update_relay_list() tool_ctl.update_relay_list()
tool_ctl.gen_modem_list()
elseif event == "peripheral" then elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns ---@diagnostic disable-next-line: discard-returns
ppm.mount(param1) ppm.mount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
tool_ctl.update_relay_list() tool_ctl.update_relay_list()
tool_ctl.gen_modem_list()
end end
if event == "terminate" then return end if event == "terminate" then return end

View File

@ -31,7 +31,7 @@ function databus.tx_versions(rtu_v, comms_v)
databus.ps.publish("comms_version", comms_v) databus.ps.publish("comms_version", comms_v)
end end
-- transmit hardware status for modem connection state -- transmit hardware status for comms modem connection state
---@param has_modem boolean ---@param has_modem boolean
function databus.tx_hw_modem(has_modem) function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem) databus.ps.publish("has_modem", has_modem)

View File

@ -36,6 +36,9 @@ function rtu.load_config()
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.RTU_Channel = settings.get("RTU_Channel") config.RTU_Channel = settings.get("RTU_Channel")
config.ConnTimeout = settings.get("ConnTimeout") config.ConnTimeout = settings.get("ConnTimeout")
config.WirelessModem = settings.get("WirelessModem")
config.WiredModem = settings.get("WiredModem")
config.PreferWireless = settings.get("PreferWireless")
config.TrustedRange = settings.get("TrustedRange") config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey") config.AuthKey = settings.get("AuthKey")
@ -61,6 +64,10 @@ function rtu.validate_config(cfg)
cfv.assert_channel(cfg.RTU_Channel) cfv.assert_channel(cfg.RTU_Channel)
cfv.assert_type_num(cfg.ConnTimeout) cfv.assert_type_num(cfg.ConnTimeout)
cfv.assert_min(cfg.ConnTimeout, 2) cfv.assert_min(cfg.ConnTimeout, 2)
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_type_num(cfg.TrustedRange) cfv.assert_type_num(cfg.TrustedRange)
cfv.assert_min(cfg.TrustedRange, 0) cfv.assert_min(cfg.TrustedRange, 0)
cfv.assert_type_str(cfg.AuthKey) cfv.assert_type_str(cfg.AuthKey)
@ -286,7 +293,7 @@ end
-- RTU Communications -- RTU Communications
---@nodiscard ---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param nic nic network interface device ---@param nic nic|nil network interface device
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, nic, conn_watchdog) function rtu.comms(version, nic, conn_watchdog)
local self = { local self = {
@ -299,28 +306,43 @@ function rtu.comms(version, nic, conn_watchdog)
local insert = table.insert local insert = table.insert
comms.set_trusted_range(config.TrustedRange) -- CONDITIONAL PRIVATE FUNCTIONS --
-- PRIVATE FUNCTIONS -- -- these don't check for nic to be nil to save execution time on functions called extremely often
-- when the nic isn't present, the aliases _send and _send_modbus are cleared
-- configure modem channels
nic.closeAll()
nic.open(config.RTU_Channel)
-- send a scada management packet -- send a scada management packet
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
local function _send(msg_type, msg) local function _nic_send(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) m_pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
---@diagnostic disable-next-line: need-check-nil
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt) nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
local function _nic_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())
---@diagnostic disable-next-line: need-check-nil
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
self.seq_num = self.seq_num + 1
end
-- PRIVATE FUNCTIONS --
-- send a scada management packet
local _send = _nic_send
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) local function _send_keep_alive_ack(srv_time)
@ -351,13 +373,7 @@ function rtu.comms(version, nic, conn_watchdog)
local public = {} local public = {}
-- send a MODBUS TCP packet -- send a MODBUS TCP packet
---@param m_pkt modbus_packet public.send_modbus = _nic_send_modbus
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
-- unlink from the server -- unlink from the server
---@param rtu_state rtu_state ---@param rtu_state rtu_state
@ -404,6 +420,8 @@ function rtu.comms(version, nic, conn_watchdog)
---@param distance integer ---@param distance integer
---@return modbus_frame|mgmt_frame|nil packet ---@return modbus_frame|mgmt_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
-- unreachable if there isn't a nic
---@diagnostic disable-next-line: need-check-nil
local s_pkt = nic.receive(side, sender, reply_to, message, distance) local s_pkt = nic.receive(side, sender, reply_to, message, distance)
local pkt = nil local pkt = nil
@ -594,6 +612,34 @@ function rtu.comms(version, nic, conn_watchdog)
end end
end end
-- set the current NIC
---@param _nic nic
function public.assign_nic(_nic)
if nic then nic.closeAll() end
if _nic.isWireless() then
comms.set_trusted_range(config.TrustedRange)
end
-- configure receive channels
_nic.closeAll()
_nic.open(config.RTU_Channel)
nic = _nic
_send = _nic_send
public.send_modbus = _nic_send_modbus
end
-- clear the current NIC
function public.unassign_nic()
_send = function () end
public.send_modbus = function () end
nic = nil
end
-- set the NIC if one was given
if nic then public.assign_nic(nic) else public.unassign_nic() end
return public return public
end end

View File

@ -1,5 +1,5 @@
-- --
-- RTU: Remote Terminal Unit -- RTU Gateway: Remote Terminal Unit Gateway
-- --
require("/initenv").init_env() require("/initenv").init_env()
@ -11,30 +11,17 @@ local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") 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 util = require("scada-common.util")
local backplane = require("rtu.backplane")
local configure = require("rtu.configure") local configure = require("rtu.configure")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu") local rtu = require("rtu.rtu")
local threads = require("rtu.threads") local threads = require("rtu.threads")
local uinit = require("rtu.uinit")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") local RTU_VERSION = "v1.13.0"
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 println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -106,15 +93,9 @@ local function main()
shutdown = false shutdown = false
}, },
-- RTU gateway devices (not RTU units)
rtu_dev = {
modem = ppm.get_wireless_modem(),
sounders = {} ---@type rtu_speaker_sounder[]
},
-- system objects -- system objects
---@class rtu_sys
rtu_sys = { rtu_sys = {
nic = nil, ---@type nic
rtu_comms = nil, ---@type rtu_comms rtu_comms = nil, ---@type rtu_comms
conn_watchdog = nil, ---@type watchdog conn_watchdog = nil, ---@type watchdog
units = {} ---@type rtu_registry_entry[] units = {} ---@type rtu_registry_entry[]
@ -127,463 +108,18 @@ local function main()
} }
local smem_sys = __shared_memory.rtu_sys local smem_sys = __shared_memory.rtu_sys
local smem_dev = __shared_memory.rtu_dev
local rtu_state = __shared_memory.rtu_state local rtu_state = __shared_memory.rtu_state
----------------------------------------
-- interpret config and init units
----------------------------------------
local units = __shared_memory.rtu_sys.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 -- start system
---------------------------------------- ----------------------------------------
log.debug("boot> running sys_config()") log.debug("boot> running uinit()")
if sys_config() then if uinit(config, __shared_memory) then
-- check modem -- init backplane peripherals
if smem_dev.modem == nil then backplane.init(config, __shared_memory)
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)
-- start UI -- start UI
local message local message
@ -601,9 +137,13 @@ local function main()
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.nic = network.nic(smem_dev.modem) local nic = backplane.active_nic()
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog) smem_sys.rtu_comms = rtu.comms(RTU_VERSION, nic, smem_sys.conn_watchdog)
if nic then
log.debug("startup> comms init") log.debug("startup> comms init")
else
log.warning("startup> no comms modem on startup")
end
-- init threads -- init threads
local main_thread = threads.thread__main(__shared_memory) local main_thread = threads.thread__main(__shared_memory)

View File

@ -5,10 +5,10 @@ local tcd = require("scada-common.tcd")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local backplane = require("rtu.backplane")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_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_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE local RTU_HW_STATE = databus.RTU_HW_STATE
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- 2Hz, 10 ticks
local COMMS_SLEEP = 100 -- (100ms, 2 ticks) local COMMS_SLEEP = 100 -- 100ms, 2 ticks
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
---@param println_ts function ---@param println_ts function
@ -191,12 +191,12 @@ function threads.thread__main(smem)
-- load in from shared memory -- load in from shared memory
local rtu_state = smem.rtu_state 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 rtu_comms = smem.rtu_sys.rtu_comms
local conn_watchdog = smem.rtu_sys.conn_watchdog local conn_watchdog = smem.rtu_sys.conn_watchdog
local units = smem.rtu_sys.units local units = smem.rtu_sys.units
local sounders = backplane.sounders()
-- start unlinked (in case of restart) -- start unlinked (in case of restart)
rtu_comms.unlink(rtu_state) rtu_comms.unlink(rtu_state)
@ -246,38 +246,8 @@ function threads.thread__main(smem)
local type, device = ppm.handle_unmount(param1) local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" or type == "speaker" then
---@cast device Modem backplane.detach(type, device, param1)
-- 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
else else
for i = 1, #units do for i = 1, #units do
-- find disconnected device -- find disconnected device
@ -301,29 +271,8 @@ function threads.thread__main(smem)
local type, device = ppm.mount(param1) local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" or type == "speaker" then
---@cast device Modem backplane.attach(type, device, param1)
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)
else else
-- relink lost peripheral to correct unit entry -- relink lost peripheral to correct unit entry
for i = 1, #units do for i = 1, #units do
@ -391,12 +340,12 @@ function threads.thread__comms(smem)
-- load in from shared memory -- load in from shared memory
local rtu_state = smem.rtu_state local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local rtu_comms = smem.rtu_sys.rtu_comms local rtu_comms = smem.rtu_sys.rtu_comms
local units = smem.rtu_sys.units local units = smem.rtu_sys.units
local comms_queue = smem.q.mq_comms local comms_queue = smem.q.mq_comms
local sounders = backplane.sounders()
local last_update = util.time() local last_update = util.time()
-- thread loop -- thread loop

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 = {} local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version) -- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "3.0.8" comms.version = "3.0.9"
comms.api_version = "0.0.10" comms.api_version = "0.0.10"
---@enum PROTOCOL ---@enum PROTOCOL
@ -49,13 +49,14 @@ local MGMT_TYPE = {
ESTABLISH = 0, -- establish new connection ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection CLOSE = 2, -- close a connection
RTU_ADVERT = 3, -- RTU capability advertisement PROBE = 3,
RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount RTU_ADVERT = 4, -- RTU capability advertisement
RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones RTU_DEV_REMOUNT = 5, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
DIAG_TONE_GET = 6, -- (API) diagnostic: get alarm tones RTU_TONE_ALARM = 6, -- instruct RTUs to play specified alarm tones
DIAG_TONE_SET = 7, -- (API) diagnostic: set alarm tones DIAG_TONE_GET = 7, -- (API) diagnostic: get alarm tones
DIAG_ALARM_SET = 8, -- (API) diagnostic: set alarm to simulate audio for DIAG_TONE_SET = 8, -- (API) diagnostic: set alarm tones
INFO_LIST_CMP = 9 -- (API) info: list all computers on the network 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 ---@enum CRDN_TYPE
@ -89,6 +90,12 @@ local ESTABLISH_ACK = {
---@enum DEVICE_TYPE device types for establish messages ---@enum DEVICE_TYPE device types for establish messages
local DEVICE_TYPE = { PLC = 0, RTU = 1, SVR = 2, CRD = 3, PKT = 4 } 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 ---@enum PLC_AUTO_ACK
local PLC_AUTO_ACK = { local PLC_AUTO_ACK = {
FAIL = 0, -- failed to set burn rate/burn rate invalid 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.ESTABLISH_ACK = ESTABLISH_ACK
comms.DEVICE_TYPE = DEVICE_TYPE comms.DEVICE_TYPE = DEVICE_TYPE
comms.PROBE_ACK = PROBE_ACK
comms.PLC_AUTO_ACK = PLC_AUTO_ACK comms.PLC_AUTO_ACK = PLC_AUTO_ACK
comms.UNIT_COMMAND = UNIT_COMMAND comms.UNIT_COMMAND = UNIT_COMMAND
@ -205,7 +214,7 @@ function comms.scada_packet()
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("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 else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 5 then if #self.raw == 5 then
@ -251,6 +260,8 @@ function comms.scada_packet()
---@nodiscard ---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
---@nodiscard
function public.interface() return self.modem_msg_in.iface end
---@nodiscard ---@nodiscard
function public.local_channel() return self.modem_msg_in.s_channel end function public.local_channel() return self.modem_msg_in.s_channel end
---@nodiscard ---@nodiscard
@ -326,7 +337,7 @@ function comms.authd_packet()
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("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 else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 4 then if #self.raw == 4 then
@ -412,7 +423,7 @@ function comms.modbus_packet()
self.raw = { self.txn_id, self.unit_id, self.func_code } self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do insert(self.raw, data[i]) end for i = 1, self.length do insert(self.raw, data[i]) end
else else
log.error("comms.modbus_packet.make(): data not a table") log.error("COMMS: modbus_packet.make(): data not a table")
end end
end end
@ -435,11 +446,11 @@ function comms.modbus_packet()
return size_ok and valid return size_ok and valid
else else
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) log.debug("COMMS: attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end
@ -498,7 +509,7 @@ function comms.rplc_packet()
self.raw = { self.id, self.type } self.raw = { self.id, self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("comms.rplc_packet.make(): data not a table") log.error("COMMS: rplc_packet.make(): data not a table")
end end
end end
@ -521,11 +532,11 @@ function comms.rplc_packet()
return ok return ok
else else
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) log.debug("COMMS: attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end
@ -580,7 +591,7 @@ function comms.mgmt_packet()
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("comms.mgmt_packet.make(): data not a table") log.error("COMMS: mgmt_packet.make(): data not a table")
end end
end end
@ -601,11 +612,11 @@ function comms.mgmt_packet()
return ok return ok
else else
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) log.debug("COMMS: attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end
@ -659,7 +670,7 @@ function comms.crdn_packet()
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("comms.crdn_packet.make(): data not a table") log.error("COMMS: crdn_packet.make(): data not a table")
end end
end end
@ -680,11 +691,11 @@ function comms.crdn_packet()
return ok return ok
else else
log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true) log.debug("COMMS: attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("nil frame encountered", true) log.debug("COMMS: nil frame encountered", true)
return false return false
end end
end end

View File

@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE log.MODE = MODE
local logger = { local _log = {
not_ready = true, not_ready = true,
path = "/log.txt", path = "/log.txt",
mode = MODE.APPEND, mode = MODE.APPEND,
@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace
---@param err_msg string|nil error message ---@param err_msg string|nil error message
---@return boolean out_of_space ---@return boolean out_of_space
local function check_out_of_space(err_msg) local function check_out_of_space(err_msg)
return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil)) return (free_space(_log.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
end end
-- private log write function -- private log write function
---@param msg_bits any[] ---@param msg_bits any[]
local function _log(msg_bits) local function write_log(msg_bits)
if logger.not_ready then return end if _log.not_ready then return end
local time_stamp = os.date(TIME_FMT) local time_stamp = os.date(TIME_FMT)
local stamped = util.c(time_stamp, table.unpack(msg_bits)) local stamped = util.c(time_stamp, table.unpack(msg_bits))
-- attempt to write log -- attempt to write log
local status, result = pcall(function () local status, result = pcall(function ()
logger.file.writeLine(stamped) _log.file.writeLine(stamped)
logger.file.flush() _log.file.flush()
end) end)
-- if we don't have space, we need to create a new log file -- if we don't have space, we need to create a new log file
if check_out_of_space() then if check_out_of_space() then
-- delete the old log file before opening a new one -- delete the old log file before opening a new one
logger.file.close() _log.file.close()
fs.delete(logger.path) fs.delete(_log.path)
-- re-init logger and pass dmesg_out so that it doesn't change -- re-init logger and pass dmesg_out so that it doesn't change
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out) log.init(_log.path, _log.mode, _log.debug, _log.dmesg_out)
-- log the message and recycle warning -- log the message and recycle warning
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file") _log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
logger.file.writeLine(stamped) _log.file.writeLine(stamped)
logger.file.flush() _log.file.flush()
elseif (not status) and (result ~= nil) then elseif (not status) and (result ~= nil) then
util.println("unexpected error writing to the log file: " .. result) util.println("unexpected error writing to the log file: " .. result)
end end
@ -89,45 +89,45 @@ end
function log.init(path, write_mode, include_debug, dmesg_redirect) function log.init(path, write_mode, include_debug, dmesg_redirect)
local err_msg local err_msg
logger.path = path _log.path = path
logger.mode = write_mode _log.mode = write_mode
logger.debug = include_debug _log.debug = include_debug
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w")) _log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
if dmesg_redirect then if dmesg_redirect then
logger.dmesg_out = dmesg_redirect _log.dmesg_out = dmesg_redirect
else else
logger.dmesg_out = term.current() _log.dmesg_out = term.current()
end end
-- check for space issues -- check for space issues
local out_of_space = check_out_of_space(err_msg) local out_of_space = check_out_of_space(err_msg)
-- try to handle problems -- try to handle problems
if logger.file == nil or out_of_space then if _log.file == nil or out_of_space then
if out_of_space then if out_of_space then
if fs.exists(logger.path) then if fs.exists(_log.path) then
fs.delete(logger.path) fs.delete(_log.path)
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w")) _log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
if logger.file then if _log.file then
logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file") _log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
logger.file.flush() _log.file.flush()
else error("failed to setup the log file: " .. err_msg) end else error("failed to setup the log file: " .. err_msg) end
else error("failed to make space for the log file, please delete unused files") end else error("failed to make space for the log file, please delete unused files") end
else error("unexpected error setting up the log file: " .. err_msg) end else error("unexpected error setting up the log file: " .. err_msg) end
end end
logger.not_ready = false _log.not_ready = false
end end
-- close the log file handle -- close the log file handle
function log.close() logger.file.close() end function log.close() _log.file.close() end
-- direct dmesg output to a monitor/window -- direct dmesg output to a monitor/window
---@param window Window window or terminal reference ---@param window Window window or terminal reference
function log.direct_dmesg(window) logger.dmesg_out = window end function log.direct_dmesg(window) _log.dmesg_out = window end
-- dmesg style logging for boot because I like linux-y things -- dmesg style logging for boot because I like linux-y things
---@param msg any message ---@param msg any message
@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color)
tag = util.strval(tag or "") tag = util.strval(tag or "")
local t_stamp = string.format("%12.2f", os.clock()) local t_stamp = string.format("%12.2f", os.clock())
local out = logger.dmesg_out local out = _log.dmesg_out
if out ~= nil then if out ~= nil then
local out_w, out_h = out.getSize() local out_w, out_h = out.getSize()
@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1 _log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1 _log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i]) out.write(lines[i])
end end
logger.dmesg_restore_coord = { out.getCursorPos() } _log.dmesg_restore_coord = { out.getCursorPos() }
_log{"[", t_stamp, "] [", tag, "] ", msg} write_log{"[", t_stamp, "] [", tag, "] ", msg}
end end
return ts_coord return ts_coord
@ -241,9 +241,9 @@ end
---@return function update, function done ---@return function update, function done
function log.dmesg_working(msg, tag, tag_color) function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color) local ts_coord = log.dmesg(msg, tag, tag_color)
local initial_scroll = logger.dmesg_scroll_count local initial_scroll = _log.dmesg_scroll_count
local out = logger.dmesg_out local out = _log.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1 local width = (ts_coord.x2 - ts_coord.x1) + 1
if out ~= nil then if out ~= nil then
@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color)
local counter = 0 local counter = 0
local function update(sec_remaining) local function update(sec_remaining)
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll) local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end if new_y < 1 then return end
local time = util.sprintf("%ds", sec_remaining) local time = util.sprintf("%ds", sec_remaining)
@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color)
counter = counter + 1 counter = counter + 1
out.setCursorPos(table.unpack(logger.dmesg_restore_coord)) out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
end end
local function done(ok) local function done(ok)
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll) local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end if new_y < 1 then return end
out.setCursorPos(ts_coord.x1, new_y) out.setCursorPos(ts_coord.x1, new_y)
@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color)
out.setTextColor(initial_color) out.setTextColor(initial_color)
out.setCursorPos(table.unpack(logger.dmesg_restore_coord)) out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
end end
return update, done return update, done
@ -312,28 +312,28 @@ end
---@param msg any message ---@param msg any message
---@param trace? boolean include file trace ---@param trace? boolean include file trace
function log.debug(msg, trace) function log.debug(msg, trace)
if logger.debug then if _log.debug then
if trace then if trace then
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.name ~= nil then if info.name ~= nil then
_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} write_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
else else
_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg} write_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
end end
else else
_log{DBG_TAG, msg} write_log{DBG_TAG, msg}
end end
end end
end end
-- log info messages -- log info messages
---@param msg any message ---@param msg any message
function log.info(msg) _log{INF_TAG, msg} end function log.info(msg) write_log{INF_TAG, msg} end
-- log warning messages -- log warning messages
---@param msg any message ---@param msg any message
function log.warning(msg) _log{WRN_TAG, msg} end function log.warning(msg) write_log{WRN_TAG, msg} end
-- log error messages -- log error messages
---@param msg any message ---@param msg any message
@ -343,17 +343,17 @@ function log.error(msg, trace)
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.name ~= nil then if info.name ~= nil then
_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} write_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
else else
_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg} write_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
end end
else else
_log{ERR_TAG, msg} write_log{ERR_TAG, msg}
end end
end end
-- log fatal errors -- log fatal errors
---@param msg any message ---@param msg any message
function log.fatal(msg) _log{FTL_TAG, msg} end function log.fatal(msg) write_log{FTL_TAG, msg} end
return log return log

View File

@ -1,9 +1,10 @@
-- --
-- Network Communications -- Network Communications and Message Authentication
-- --
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5") local md5 = require("lockbox.digest.md5")
@ -17,7 +18,7 @@ local array = require("lockbox.util.array")
local network = {} local network = {}
-- cryptography engine -- cryptography engine
local c_eng = { local _crypt = {
key = nil, key = nil,
hmac = nil hmac = nil
} }
@ -39,23 +40,23 @@ function network.init_mac(passkey)
key_deriv.setPassword(passkey) key_deriv.setPassword(passkey)
key_deriv.finish() key_deriv.finish()
c_eng.key = array.fromHex(key_deriv.asHex()) _crypt.key = array.fromHex(key_deriv.asHex())
-- initialize HMAC -- initialize HMAC
c_eng.hmac = hmac() _crypt.hmac = hmac()
c_eng.hmac.setBlockSize(64) _crypt.hmac.setBlockSize(64)
c_eng.hmac.setDigest(md5) _crypt.hmac.setDigest(md5)
c_eng.hmac.setKey(c_eng.key) _crypt.hmac.setKey(_crypt.key)
local init_time = util.time_ms() - start local init_time = util.time_ms() - start
log.info("network.init_mac completed in " .. init_time .. "ms") log.info("NET: network.init_mac() completed in " .. init_time .. "ms")
return init_time return init_time
end end
-- de-initialize message authentication system -- de-initialize message authentication system
function network.deinit_mac() function network.deinit_mac()
c_eng.key, c_eng.hmac = nil, nil _crypt.key, _crypt.hmac = nil, nil
end end
-- generate HMAC of message -- generate HMAC of message
@ -64,29 +65,41 @@ end
local function compute_hmac(message) local function compute_hmac(message)
-- local start = util.time_ms() -- local start = util.time_ms()
c_eng.hmac.init() _crypt.hmac.init()
c_eng.hmac.update(stream.fromString(message)) _crypt.hmac.update(stream.fromString(message))
c_eng.hmac.finish() _crypt.hmac.finish()
local hash = c_eng.hmac.asHex() local hash = _crypt.hmac.asHex()
-- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)") -- log.debug("NET: compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
return hash return hash
end end
-- NIC: Network Interface Controller<br> -- NIC: Network Interface Controller<br>
-- utilizes HMAC-MD5 for message authentication, if enabled -- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless
---@param modem Modem modem to use ---@param modem Modem|nil modem to use
function network.nic(modem) function network.nic(modem)
local self = { local self = {
connected = true, -- used to avoid costly MAC calculations if modem isn't even present -- modem interface name
iface = "?",
-- phy name
name = "?",
-- used to quickly return out of tx/rx functions if there is nothing to do
connected = false,
-- used to avoid costly MAC calculations if not required
use_hash = false,
-- open channels
channels = {} channels = {}
} }
---@class nic:Modem ---@class nic:Modem
local public = {} local public = {}
-- get the phy name
---@nodiscard
function public.phy_name() return self.name end
-- check if this NIC has a connected modem -- check if this NIC has a connected modem
---@nodiscard ---@nodiscard
function public.is_connected() return self.connected end function public.is_connected() return self.connected end
@ -95,9 +108,14 @@ function network.nic(modem)
---@param reconnected_modem Modem ---@param reconnected_modem Modem
function public.connect(reconnected_modem) function public.connect(reconnected_modem)
modem = reconnected_modem modem = reconnected_modem
self.connected = true
-- open previously opened channels self.iface = ppm.get_iface(modem)
self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}")
self.connected = true
self.use_hash = _crypt.hmac and modem.isWireless()
-- open only previously opened channels
modem.closeAll()
for _, channel in ipairs(self.channels) do for _, channel in ipairs(self.channels) do
modem.open(channel) modem.open(channel)
end end
@ -117,13 +135,13 @@ function network.nic(modem)
function public.is_modem(device) return device == modem end function public.is_modem(device) return device == modem end
-- wrap modem functions, then create custom functions -- wrap modem functions, then create custom functions
public.connect(modem) if modem then public.connect(modem) end
-- open a channel on the modem<br> -- open a channel on the modem<br>
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection -- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
---@param channel integer ---@param channel integer
function public.open(channel) function public.open(channel)
modem.open(channel) if modem then modem.open(channel) end
local already_open = false local already_open = false
for i = 1, #self.channels do for i = 1, #self.channels do
@ -141,7 +159,7 @@ function network.nic(modem)
-- close a channel on the modem -- close a channel on the modem
---@param channel integer ---@param channel integer
function public.close(channel) function public.close(channel)
modem.close(channel) if modem then modem.close(channel) end
for i = 1, #self.channels do for i = 1, #self.channels do
if self.channels[i] == channel then if self.channels[i] == channel then
@ -153,7 +171,7 @@ function network.nic(modem)
-- close all channels on the modem -- close all channels on the modem
function public.closeAll() function public.closeAll()
modem.closeAll() if modem then modem.closeAll() end
self.channels = {} self.channels = {}
end end
@ -165,17 +183,20 @@ function network.nic(modem)
if self.connected then if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet local tx_packet = packet ---@type authd_packet|scada_packet
if c_eng.hmac ~= nil then if self.use_hash then
-- local start = util.time_ms() -- local start = util.time_ms()
tx_packet = comms.authd_packet() tx_packet = comms.authd_packet()
---@cast tx_packet authd_packet ---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac) tx_packet.make(packet, compute_hmac)
-- log.debug("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 end
---@diagnostic disable-next-line: need-check-nil
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable()) modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
else
log.debug("NET: network.transmit() tx dropped, link is down")
end end
end end
@ -190,10 +211,10 @@ function network.nic(modem)
function public.receive(side, sender, reply_to, message, distance) function public.receive(side, sender, reply_to, message, distance)
local packet = nil local packet = nil
if self.connected then if self.connected and side == self.iface then
local s_packet = comms.scada_packet() local s_packet = comms.scada_packet()
if c_eng.hmac ~= nil then if self.use_hash then
-- parse packet as an authenticated SCADA packet -- parse packet as an authenticated SCADA packet
local a_packet = comms.authd_packet() local a_packet = comms.authd_packet()
a_packet.receive(side, sender, reply_to, message, distance) a_packet.receive(side, sender, reply_to, message, distance)
@ -206,10 +227,10 @@ function network.nic(modem)
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true })) local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then if a_packet.mac() == computed_hmac then
-- log.debug("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() s_packet.stamp_authenticated()
else else
-- log.debug("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 end
end end

View File

@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
local REPORT_FREQUENCY = 20 -- log every 20 faults per function local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local ppm_sys = { local _ppm = {
mounts = {}, ---@type { [string]: ppm_entry } mounts = {}, ---@type { [string]: ppm_entry }
next_vid = 0, next_vid = 0,
auto_cf = false, auto_cf = false,
@ -66,7 +66,7 @@ local function peri_init(iface)
if status then if status then
-- auto fault clear -- auto fault clear
if self.auto_cf then self.faulted = false end if self.auto_cf then self.faulted = false end
if ppm_sys.auto_cf then ppm_sys.faulted = false end if _ppm.auto_cf then _ppm.faulted = false end
self.fault_counts[key] = 0 self.fault_counts[key] = 0
@ -78,10 +78,10 @@ local function peri_init(iface)
self.faulted = true self.faulted = true
self.last_fault = result self.last_fault = result
ppm_sys.faulted = true _ppm.faulted = true
ppm_sys.last_fault = result _ppm.last_fault = result
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = "" local count_str = ""
if self.fault_counts[key] > 0 then if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total faults]" count_str = " [" .. self.fault_counts[key] .. " total faults]"
@ -92,7 +92,7 @@ local function peri_init(iface)
self.fault_counts[key] = self.fault_counts[key] + 1 self.fault_counts[key] = self.fault_counts[key] + 1
if result == "Terminated" then ppm_sys.terminate = true end if result == "Terminated" then _ppm.terminate = true end
return ACCESS_FAULT, result return ACCESS_FAULT, result
end end
@ -159,10 +159,10 @@ local function peri_init(iface)
self.faulted = true self.faulted = true
self.last_fault = UNDEFINED_FIELD self.last_fault = UNDEFINED_FIELD
ppm_sys.faulted = true _ppm.faulted = true
ppm_sys.last_fault = UNDEFINED_FIELD _ppm.last_fault = UNDEFINED_FIELD
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = "" local count_str = ""
if self.fault_counts[key] > 0 then if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total calls]" count_str = " [" .. self.fault_counts[key] .. " total calls]"
@ -193,35 +193,35 @@ end
-- REPORTING -- -- REPORTING --
-- silence error prints -- silence error prints
function ppm.disable_reporting() ppm_sys.mute = true end function ppm.disable_reporting() _ppm.mute = true end
-- allow error prints -- allow error prints
function ppm.enable_reporting() ppm_sys.mute = false end function ppm.enable_reporting() _ppm.mute = false end
-- FAULT MEMORY -- -- FAULT MEMORY --
-- enable automatically clearing fault flag -- enable automatically clearing fault flag
function ppm.enable_afc() ppm_sys.auto_cf = true end function ppm.enable_afc() _ppm.auto_cf = true end
-- disable automatically clearing fault flag -- disable automatically clearing fault flag
function ppm.disable_afc() ppm_sys.auto_cf = false end function ppm.disable_afc() _ppm.auto_cf = false end
-- clear fault flag -- clear fault flag
function ppm.clear_fault() ppm_sys.faulted = false end function ppm.clear_fault() _ppm.faulted = false end
-- check fault flag -- check fault flag
---@nodiscard ---@nodiscard
function ppm.is_faulted() return ppm_sys.faulted end function ppm.is_faulted() return _ppm.faulted end
-- get the last fault message -- get the last fault message
---@nodiscard ---@nodiscard
function ppm.get_last_fault() return ppm_sys.last_fault end function ppm.get_last_fault() return _ppm.last_fault end
-- TERMINATION -- -- TERMINATION --
-- if a caught error was a termination request -- if a caught error was a termination request
---@nodiscard ---@nodiscard
function ppm.should_terminate() return ppm_sys.terminate end function ppm.should_terminate() return _ppm.terminate end
-- MOUNTING -- -- MOUNTING --
@ -229,12 +229,12 @@ function ppm.should_terminate() return ppm_sys.terminate end
function ppm.mount_all() function ppm.mount_all()
local ifaces = peripheral.getNames() local ifaces = peripheral.getNames()
ppm_sys.mounts = {} _ppm.mounts = {}
for i = 1, #ifaces do for i = 1, #ifaces do
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i]) _ppm.mounts[ifaces[i]] = peri_init(ifaces[i])
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")")) log.info(util.c("PPM: found a ", _ppm.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
end end
if #ifaces == 0 then if #ifaces == 0 then
@ -253,10 +253,10 @@ function ppm.mount(iface)
for i = 1, #ifaces do for i = 1, #ifaces do
if iface == ifaces[i] then if iface == ifaces[i] then
ppm_sys.mounts[iface] = peri_init(iface) _ppm.mounts[iface] = peri_init(iface)
pm_type = ppm_sys.mounts[iface].type pm_type = _ppm.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev pm_dev = _ppm.mounts[iface].dev
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type)) log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
break break
@ -278,12 +278,12 @@ function ppm.remount(iface)
for i = 1, #ifaces do for i = 1, #ifaces do
if iface == ifaces[i] then if iface == ifaces[i] then
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type)) log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type))
ppm.unmount(ppm_sys.mounts[iface].dev) ppm.unmount(_ppm.mounts[iface].dev)
ppm_sys.mounts[iface] = peri_init(iface) _ppm.mounts[iface] = peri_init(iface)
pm_type = ppm_sys.mounts[iface].type pm_type = _ppm.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev pm_dev = _ppm.mounts[iface].dev
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type)) log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
break break
@ -293,28 +293,28 @@ function ppm.remount(iface)
return pm_type, pm_dev return pm_type, pm_dev
end end
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices) -- mount a virtual placeholder device
---@nodiscard ---@nodiscard
---@return string type, table device ---@return string type, table device
function ppm.mount_virtual() function ppm.mount_virtual()
local iface = "ppm_vdev_" .. ppm_sys.next_vid local iface = "ppm_vdev_" .. _ppm.next_vid
ppm_sys.mounts[iface] = peri_init("__virtual__") _ppm.mounts[iface] = peri_init("__virtual__")
ppm_sys.next_vid = ppm_sys.next_vid + 1 _ppm.next_vid = _ppm.next_vid + 1
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface)) log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev return _ppm.mounts[iface].type, _ppm.mounts[iface].dev
end end
-- manually unmount a peripheral from the PPM -- manually unmount a peripheral from the PPM
---@param device table device table ---@param device table device table
function ppm.unmount(device) function ppm.unmount(device)
if device then if device then
for iface, data in pairs(ppm_sys.mounts) do for iface, data in pairs(_ppm.mounts) do
if data.dev == device then if data.dev == device then
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface)) log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
ppm_sys.mounts[iface] = nil _ppm.mounts[iface] = nil
break break
end end
end end
@ -330,7 +330,7 @@ function ppm.handle_unmount(iface)
local pm_type = nil local pm_type = nil
-- what got disconnected? -- what got disconnected?
local lost_dev = ppm_sys.mounts[iface] local lost_dev = _ppm.mounts[iface]
if lost_dev then if lost_dev then
pm_type = lost_dev.type pm_type = lost_dev.type
@ -341,18 +341,18 @@ function ppm.handle_unmount(iface)
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface)) log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
end end
ppm_sys.mounts[iface] = nil _ppm.mounts[iface] = nil
return pm_type, pm_dev return pm_type, pm_dev
end end
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready -- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
function ppm.log_mounts() function ppm.log_mounts()
for iface, mount in pairs(ppm_sys.mounts) do for iface, mount in pairs(_ppm.mounts) do
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")")) log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
end end
if util.table_len(ppm_sys.mounts) == 0 then if util.table_len(_ppm.mounts) == 0 then
log.warning("PPM: no devices had been found") log.warning("PPM: no devices had been found")
end end
end end
@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end
---@return { [string]: ppm_entry } mounts ---@return { [string]: ppm_entry } mounts
function ppm.list_mounts() function ppm.list_mounts()
local list = {} local list = {}
for k, v in pairs(ppm_sys.mounts) do list[k] = v end for k, v in pairs(_ppm.mounts) do list[k] = v end
return list return list
end end
@ -379,7 +379,7 @@ end
---@return string|nil iface CC peripheral interface ---@return string|nil iface CC peripheral interface
function ppm.get_iface(device) function ppm.get_iface(device)
if device then if device then
for iface, data in pairs(ppm_sys.mounts) do for iface, data in pairs(_ppm.mounts) do
if data.dev == device then return iface end if data.dev == device then return iface end
end end
end end
@ -392,8 +392,8 @@ end
---@param iface string CC peripheral interface ---@param iface string CC peripheral interface
---@return { [string]: function }|nil device function table ---@return { [string]: function }|nil device function table
function ppm.get_periph(iface) function ppm.get_periph(iface)
if ppm_sys.mounts[iface] then if _ppm.mounts[iface] then
return ppm_sys.mounts[iface].dev return _ppm.mounts[iface].dev
else return nil end else return nil end
end end
@ -402,20 +402,20 @@ end
---@param iface string CC peripheral interface ---@param iface string CC peripheral interface
---@return string|nil type ---@return string|nil type
function ppm.get_type(iface) function ppm.get_type(iface)
if ppm_sys.mounts[iface] then if _ppm.mounts[iface] then
return ppm_sys.mounts[iface].type return _ppm.mounts[iface].type
else return nil end else return nil end
end end
-- get all mounted peripherals by type -- get all mounted peripherals by type
---@nodiscard ---@nodiscard
---@param name string type name ---@param type string type name
---@return table devices device function tables ---@return table devices device function tables
function ppm.get_all_devices(name) function ppm.get_all_devices(type)
local devices = {} local devices = {}
for _, data in pairs(ppm_sys.mounts) do for _, data in pairs(_ppm.mounts) do
if data.type == name then if data.type == type then
table.insert(devices, data.dev) table.insert(devices, data.dev)
end end
end end
@ -430,7 +430,7 @@ end
function ppm.get_device(name) function ppm.get_device(name)
local device = nil local device = nil
for _, data in pairs(ppm_sys.mounts) do for _, data in pairs(_ppm.mounts) do
if data.type == name then if data.type == name then
device = data.dev device = data.dev
break break
@ -447,22 +447,49 @@ end
---@return table|nil reactor function table ---@return table|nil reactor function table
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end 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> -- get the wireless modem (if multiple, returns the first)<br>
-- if this is in a CraftOS emulated environment, wired modems will be used instead -- if this is in a CraftOS emulated environment, wired modems will be used instead
---@nodiscard ---@nodiscard
---@return Modem|nil modem function table ---@return Modem|nil modem, string|nil iface
function ppm.get_wireless_modem() function ppm.get_wireless_modem()
local w_modem = nil local w_modem, w_iface = nil, nil
local emulated_env = periphemu ~= nil local emulated_env = periphemu ~= nil
for _, device in pairs(ppm_sys.mounts) do for iface, device in pairs(_ppm.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
w_iface = iface
w_modem = device.dev w_modem = device.dev
break break
end end
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 end
-- list all connected monitors -- list all connected monitors
@ -471,7 +498,7 @@ end
function ppm.get_monitor_list() function ppm.get_monitor_list()
local list = {} local list = {}
for iface, device in pairs(ppm_sys.mounts) do for iface, device in pairs(_ppm.mounts) do
if device.type == "monitor" then list[iface] = device end if device.type == "monitor" then list[iface] = device end
end end

View File

@ -212,6 +212,13 @@ end
--#region ENUMERATION TYPES --#region ENUMERATION TYPES
---@enum LISTEN_MODE
types.LISTEN_MODE = {
WIRELESS = 1,
WIRED = 2,
ALL = 3
}
---@enum TEMP_SCALE ---@enum TEMP_SCALE
types.TEMP_SCALE = { types.TEMP_SCALE = {
KELVIN = 1, KELVIN = 1,

View File

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

187
supervisor/backplane.lua Normal file
View File

@ -0,0 +1,187 @@
--
-- Supervisor System Core Peripheral Backplane
--
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")
local databus = require("supervisor.databus")
local LISTEN_MODE = types.LISTEN_MODE
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
}
backplane.nics = _bp.nic_map
-- initialize the system peripheral backplane
---@param config svr_config
---@return boolean success
function backplane.init(config)
-- setup the wired modem, if configured
if type(config.WiredModem) == "string" then
_bp.lan_iface = config.WiredModem
local modem = ppm.get_modem(_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
local nic = network.nic(modem)
_bp.wd_nic = nic
_bp.nic_map[_bp.lan_iface] = nic
nic.closeAll()
if config.PLC_Listen ~= LISTEN_MODE.WIRELESS then nic.open(config.PLC_Channel) end
if config.RTU_Listen ~= LISTEN_MODE.WIRELESS then nic.open(config.RTU_Channel) end
if config.CRD_Listen ~= LISTEN_MODE.WIRELESS then nic.open(config.CRD_Channel) end
databus.tx_hw_wd_modem(true)
end
-- setup the wireless modem, if configured
if config.WirelessModem then
local modem, iface = ppm.get_wireless_modem()
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
local nic = network.nic(modem)
_bp.wl_nic = nic
_bp.nic_map[iface] = nic
nic.closeAll()
if config.PLC_Listen ~= LISTEN_MODE.WIRED then nic.open(config.PLC_Channel) end
if config.RTU_Listen ~= LISTEN_MODE.WIRED then nic.open(config.RTU_Channel) end
if config.CRD_Listen ~= LISTEN_MODE.WIRED then nic.open(config.CRD_Channel) end
if config.PocketEnabled then nic.open(config.PKT_Channel) end
databus.tx_hw_wl_modem(true)
end
if not ((type(config.WiredModem) == "string" or config.WirelessModem)) then
println("startup> no modems configured")
log.fatal("BKPLN: no modems configured")
return false
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))
local is_wd = _bp.wd_nic and (_bp.lan_iface == iface)
local is_wl = _bp.wl_nic and (not _bp.wl_nic.is_connected()) and m_is_wl
if is_wd 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 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
print_no_fp("standby wireless modem connected")
log.info("BKPLN: standby wireless modem connected")
else
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
local m_is_wl = device.isWireless()
local was_wd = _bp.wd_nic and _bp.wd_nic.is_modem(device)
local was_wl = _bp.wl_nic and _bp.wl_nic.is_modem(device)
log.info(util.c("BKPLN: ", util.trinary(m_is_wl, "WIRELESS", "WIRED"), " PHY_DETACH ", iface))
_bp.nic_map[iface] = nil
if _bp.wd_nic and was_wd 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 was_wl 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
elseif _bp.wl_nic and m_is_wl 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

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

View File

@ -1,4 +1,5 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -14,6 +15,7 @@ local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox") local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton") local PushButton = require("graphics.elements.controls.PushButton")
local Radio2D = require("graphics.elements.controls.Radio2D")
local RadioButton = require("graphics.elements.controls.RadioButton") local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField") local NumberField = require("graphics.elements.form.NumberField")
@ -25,14 +27,22 @@ local tri = util.trinary
local cpair = core.cpair local cpair = core.cpair
local LISTEN_MODE = types.LISTEN_MODE
local RIGHT = core.ALIGN.RIGHT local RIGHT = core.ALIGN.RIGHT
local self = { local self = {
importing_legacy = false, importing_legacy = false,
update_net_cfg = nil, ---@type function
show_auth_key = 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 show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox auth_key_textbox = nil, ---@type TextBox
auth_key_value = "" auth_key_value = ""
} }
@ -62,115 +72,220 @@ 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_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_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_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_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=1,text="Please select the network interface(s)."}
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=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 function on_wired_change(_) tool_ctl.gen_modem_list() end
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}
TextBox{parent=net_c_1,x=1,y=9,width=11,text="PLC Channel"} 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)}
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=24,y=3,text="(required for Pocket)",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=29,y=9,height=4,text="[PLC_CHANNEL]",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="MUST ONLY connect to SCADA computers",fg_bg=cpair(colors.red,colors._INHERIT)}
TextBox{parent=net_c_1,x=3,y=7,text="connecting to peripherals will cause problems",fg_bg=g_lg_fg_bg}
local modem_list = ListBox{parent=net_c_1,x=1,y=8,height=5,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="RTU Gateway Channel"} 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 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}
TextBox{parent=net_c_1,x=1,y=11,width=19,text="Coordinator Channel"} local function submit_interfaces()
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} tmp_cfg.WirelessModem = wireless.get_value()
TextBox{parent=net_c_1,x=29,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"} if not wired.get_value() then
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} tmp_cfg.WiredModem = false
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} 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=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}
self.pkt_test = Checkbox{parent=net_c_2,label="Enable Pocket Remote System Testing",default=ini_cfg.PocketEnabled,box_fg_bg=cpair(colors.lightBlue,colors.black),disable_fg_bg=g_lg_fg_bg}
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 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 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 svr_c ~= nil and plc_c ~= nil and rtu_c ~= nil and crd_c ~= nil and pkt_c ~= nil then 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.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 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) chan_err.hide(true)
else chan_err.show() end else chan_err.show() end
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_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_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=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_4,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=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"} TextBox{parent=net_c_4,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} 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"} TextBox{parent=net_c_4,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} 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"} TextBox{parent=net_c_4,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} 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"} TextBox{parent=net_c_4,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} 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 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 plc_cto ~= nil and rtu_cto ~= nil and crd_cto ~= nil and pkt_cto ~= nil then 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 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)
ct_err.hide(true) ct_err.hide(true)
else
tmp_cfg.TrustedRange = 0
tmp_cfg.AuthKey = ""
main_pane.set_value(4)
end
else ct_err.show() end else ct_err.show() end
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_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_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=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_5,x=1,y=1,text="Please set the wireless 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_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_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=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 function submit_tr()
local range_val = tonumber(range.get_value()) local range_val = tonumber(range.get_value())
if range_val ~= nil then if range_val ~= nil then
tmp_cfg.TrustedRange = range_val tmp_cfg.TrustedRange = range_val
net_pane.set_value(4) net_pane.set_value(6)
tr_err.hide(true) tr_err.hide(true)
else tr_err.show() end else tr_err.show() end
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_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_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=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_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_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=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"} TextBox{parent=net_c_6,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 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 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) hide_key.set_value(true)
censor_key(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 function submit_auth()
local v = key.get_value() local v = key.get_value()
@ -181,8 +296,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
else key_err.show() end else key_err.show() end
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_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_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=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
@ -195,7 +310,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=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,text="Log File Mode"} 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"} 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} 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 +352,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=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"} 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."} 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 +489,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.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.tank_mode, ini_cfg.FacilityTankMode) 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(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel) try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(rtu_chan, ini_cfg.RTU_Channel) try_set(rtu_chan, ini_cfg.RTU_Channel)
try_set(crd_chan, ini_cfg.CRD_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(plc_timeout, ini_cfg.PLC_Timeout)
try_set(rtu_timeout, ini_cfg.RTU_Timeout) try_set(rtu_timeout, ini_cfg.RTU_Timeout)
try_set(crd_timeout, ini_cfg.CRD_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(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey) try_set(key, ini_cfg.AuthKey)
try_set(mode, ini_cfg.LogMode) try_set(mode, ini_cfg.LogMode)
@ -406,6 +528,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]) try_set(tool_ctl.aux_cool_elems[i].enable, ini_cfg.AuxiliaryCoolant[i])
end 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.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
tool_ctl.view_cfg.enable() tool_ctl.view_cfg.enable()
@ -470,6 +603,39 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
--#region Tool Functions --#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 -- load a legacy config file
function tool_ctl.load_legacy() function tool_ctl.load_legacy()
local config = require("supervisor.config") local config = require("supervisor.config")
@ -524,6 +690,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) 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.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.PLC_Channel = config.PLC_CHANNEL tmp_cfg.PLC_Channel = config.PLC_CHANNEL
tmp_cfg.RTU_Channel = config.RTU_CHANNEL tmp_cfg.RTU_Channel = config.RTU_CHANNEL
@ -547,12 +716,6 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
self.importing_legacy = true self.importing_legacy = true
end 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 -- generate the summary list
---@param cfg svr_config ---@param cfg svr_config
function tool_ctl.gen_summary(cfg) function tool_ctl.gen_summary(cfg)
@ -675,6 +838,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end end
if val == "" then val = "no auxiliary coolant" 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 end
if not skip then if not skip then
@ -703,6 +870,59 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end end
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 --#endregion
end end

View File

@ -3,7 +3,9 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local facility = require("supervisor.config.facility") local facility = require("supervisor.config.facility")
@ -31,7 +33,8 @@ local CENTER = core.ALIGN.CENTER
local changes = { local changes = {
{ "v1.2.12", { "Added front panel UI theme", "Added color accessibility modes" } }, { "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.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 both wired and wireless networking" } }
} }
---@class svr_configurator ---@class svr_configurator
@ -67,13 +70,16 @@ local tool_ctl = {
num_units = nil, ---@type NumberField num_units = nil, ---@type NumberField
en_fac_tanks = nil, ---@type Checkbox en_fac_tanks = nil, ---@type Checkbox
tank_mode = nil, ---@type RadioButton tank_mode = nil, ---@type RadioButton
tank_fluid_opts = {}, ---@type Radio2D[]
gen_summary = nil, ---@type function gen_summary = nil, ---@type function
load_legacy = nil, ---@type function load_legacy = nil, ---@type function
cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[] cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[]
tank_elems = {}, ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[] 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 ---@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 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 AuxiliaryCoolant = {}, ---@type boolean[] if a unit has auxiliary coolant
ExtChargeIdling = false, ExtChargeIdling = false,
WirelessModem = true, ---@type boolean
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, ---@type boolean
PocketTest = true, ---@type boolean
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer PLC_Channel = nil, ---@type integer
RTU_Channel = nil, ---@type integer RTU_Channel = nil, ---@type integer
@ -121,6 +134,13 @@ local fields = {
{ "TankFluidTypes", "Tank Fluid Types", {} }, { "TankFluidTypes", "Tank Fluid Types", {} },
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} }, { "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
{ "ExtChargeIdling", "Extended Charge Idling", false }, { "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 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 }, { "PLC_Channel", "PLC Channel", 16241 },
{ "RTU_Channel", "RTU Channel", 16242 }, { "RTU_Channel", "RTU Channel", 16242 },
@ -286,11 +306,14 @@ function configurator.configure(ask_config)
tool_ctl.has_config = load_settings(ini_cfg) tool_ctl.has_config = load_settings(ini_cfg)
-- these need to be initialized as they are used before being set -- 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.FacilityTankMode = ini_cfg.FacilityTankMode
tmp_cfg.TankFluidTypes = { table.unpack(ini_cfg.TankFluidTypes) } tmp_cfg.TankFluidTypes = { table.unpack(ini_cfg.TankFluidTypes) }
reset_term() reset_term()
ppm.mount_all()
-- set overridden colors -- set overridden colors
for i = 1, #style.colors do for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex) 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} local display = DisplayBox{window=term.current(),fg_bg=style.root}
config_view(display) config_view(display)
tool_ctl.gen_modem_list()
while true do while true do
local event, param1, param2, param3 = util.pull_event() 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 if k_e then display.handle_key(k_e) end
elseif event == "paste" then elseif event == "paste" then
display.handle_paste(param1) 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 end
if event == "terminate" then return end if event == "terminate" then return end

View File

@ -24,10 +24,16 @@ function databus.tx_versions(sv_v, comms_v)
databus.ps.publish("comms_version", comms_v) databus.ps.publish("comms_version", comms_v)
end end
-- transmit hardware status for modem connection state -- transmit hardware status for the wireless comms modem connection state
---@param has_modem boolean ---@param has_modem boolean
function databus.tx_hw_modem(has_modem) function databus.tx_hw_wl_modem(has_modem)
databus.ps.publish("has_modem", has_modem) databus.ps.publish("has_wl_modem", has_modem)
end
-- transmit hardware status for the wired comms modem connection state
---@param has_modem boolean
function databus.tx_hw_wd_modem(has_modem)
databus.ps.publish("has_wd_modem", has_modem)
end end
-- transmit PLC firmware version and session connection state -- transmit PLC firmware version and session connection state

View File

@ -34,7 +34,8 @@ local ind_grn = style.ind_grn
-- create new front panel view -- create new front panel view
---@param panel DisplayBox main displaybox ---@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_box = style.theme.highlight_box
local s_hi_bright = style.theme.highlight_box_bright local s_hi_bright = style.theme.highlight_box_bright
@ -53,7 +54,7 @@ local function init(panel)
local main_page = Div{parent=page_div,x=1,y=1} 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 on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
@ -62,14 +63,23 @@ local function init(panel)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=ind_grn} if config.WirelessModem then
local wl_modem = LED{parent=system,label="WL MODEM",colors=ind_grn}
system.line_break() system.line_break()
modem.register(databus.ps, "has_modem", modem.update) wl_modem.register(databus.ps, "has_wl_modem", wl_modem.update)
end
if config.WiredModem then
local wd_modem = LED{parent=system,label="WD MODEM",colors=ind_grn}
system.line_break()
wd_modem.register(databus.ps, "has_wd_modem", wd_modem.update)
end
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID()) 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} TextBox{parent=system,x=12,y=4,width=6,text=comp_id,fg_bg=style.fp.disabled_fg}
-- --
-- about footer -- about footer

View File

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

View File

@ -41,9 +41,8 @@ svsessions.SESSION_TYPE = SESSION_TYPE
local self = { local self = {
-- references to supervisor state and other data -- references to supervisor state and other data
nic = nil, ---@type nic|nil
fp_ok = false, fp_ok = false,
config = nil, ---@type svr_config config = nil, ---@type svr_config|nil
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
plc_ini_reset = {}, plc_ini_reset = {},
-- lists of connected sessions -- lists of connected sessions
@ -55,7 +54,6 @@ local self = {
crd = {}, ---@type crd_session_struct[] crd = {}, ---@type crd_session_struct[]
pdg = {} ---@type pdg_session_struct[] pdg = {} ---@type pdg_session_struct[]
}, },
---@diagnostic enable: missing-fields
-- next session IDs -- next session IDs
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }, next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 },
-- rtu device tracking and invalid assignment detection -- rtu device tracking and invalid assignment detection
@ -84,7 +82,7 @@ local function _sv_handle_outq(session)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent -- 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 elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification -- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@ -140,12 +138,9 @@ end
local function _iterate(sessions) local function _iterate(sessions)
for i = 1, #sessions do for i = 1, #sessions do
local session = sessions[i] local session = sessions[i]
if session.open and session.instance.iterate() then if session.open and session.instance.iterate() then
_sv_handle_outq(session) _sv_handle_outq(session)
else else session.open = false end
session.open = false
end
end end
end end
@ -159,7 +154,7 @@ local function _shutdown(session)
while session.out_queue.ready() do while session.out_queue.ready() do
local msg = session.out_queue.pop() local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then 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
end end
@ -359,12 +354,10 @@ function svsessions.check_rtu_id(unit, list, max)
end end
-- initialize svsessions -- initialize svsessions
---@param nic nic network interface device
---@param fp_ok boolean front panel active ---@param fp_ok boolean front panel active
---@param config svr_config supervisor configuration ---@param config svr_config supervisor configuration
---@param facility facility ---@param facility facility
function svsessions.init(nic, fp_ok, config, facility) function svsessions.init(fp_ok, config, facility)
self.nic = nic
self.fp_ok = fp_ok self.fp_ok = fp_ok
self.config = config self.config = config
self.facility = facility self.facility = facility
@ -467,12 +460,13 @@ end
-- establish a new PLC session -- establish a new PLC session
---@nodiscard ---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer PLC computer ID ---@param source_addr integer PLC computer ID
---@param i_seq_num integer initial (most recent) sequence number ---@param i_seq_num integer initial (most recent) sequence number
---@param for_reactor integer unit ID ---@param for_reactor integer unit ID
---@param version string PLC version ---@param version string PLC version
---@return integer|false session_id ---@return integer|false session_id
function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, version) 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 if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then
---@class plc_session_struct ---@class plc_session_struct
local plc_s = { local plc_s = {
@ -480,6 +474,7 @@ function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, v
open = true, open = true,
reactor = for_reactor, reactor = for_reactor,
version = version, version = version,
nic = nic,
r_chan = self.config.PLC_Channel, r_chan = self.config.PLC_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
@ -517,17 +512,19 @@ end
-- establish a new RTU gateway session -- establish a new RTU gateway session
---@nodiscard ---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer RTU gateway computer ID ---@param source_addr integer RTU gateway computer ID
---@param i_seq_num integer initial (most recent) sequence number ---@param i_seq_num integer initial (most recent) sequence number
---@param advertisement table RTU capability advertisement ---@param advertisement table RTU capability advertisement
---@param version string RTU gateway version ---@param version string RTU gateway version
---@return integer session_id ---@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 ---@class rtu_session_struct
local rtu_s = { local rtu_s = {
s_type = "rtu", s_type = "rtu",
open = true, open = true,
version = version, version = version,
nic = nic,
r_chan = self.config.RTU_Channel, r_chan = self.config.RTU_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
@ -558,17 +555,19 @@ end
-- establish a new coordinator session -- establish a new coordinator session
---@nodiscard ---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer coordinator computer ID ---@param source_addr integer coordinator computer ID
---@param i_seq_num integer initial (most recent) sequence number ---@param i_seq_num integer initial (most recent) sequence number
---@param version string coordinator version ---@param version string coordinator version
---@return integer|false session_id ---@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 if svsessions.get_crd_session() == nil then
---@class crd_session_struct ---@class crd_session_struct
local crd_s = { local crd_s = {
s_type = "crd", s_type = "crd",
open = true, open = true,
version = version, version = version,
nic = nic,
r_chan = self.config.CRD_Channel, r_chan = self.config.CRD_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),
@ -603,16 +602,18 @@ end
-- establish a new pocket diagnostics session -- establish a new pocket diagnostics session
---@nodiscard ---@nodiscard
---@param nic nic interface to use for this session
---@param source_addr integer pocket computer ID ---@param source_addr integer pocket computer ID
---@param i_seq_num integer initial (most recent) sequence number ---@param i_seq_num integer initial (most recent) sequence number
---@param version string pocket version ---@param version string pocket version
---@return integer|false session_id ---@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 ---@class pdg_session_struct
local pdg_s = { local pdg_s = {
s_type = "pkt", s_type = "pkt",
open = true, open = true,
version = version, version = version,
nic = nic,
r_chan = self.config.PKT_Channel, r_chan = self.config.PKT_Channel,
s_addr = source_addr, s_addr = source_addr,
in_queue = mqueue.new(), in_queue = mqueue.new(),

View File

@ -15,6 +15,7 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local backplane = require("supervisor.backplane")
local configure = require("supervisor.configure") local configure = require("supervisor.configure")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local facility = require("supervisor.facility") local facility = require("supervisor.facility")
@ -23,7 +24,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.7.1" local SUPERVISOR_VERSION = "v1.8.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -125,18 +126,11 @@ local function main()
network.init_mac(config.AuthKey) network.init_mac(config.AuthKey)
end end
-- get modem -- hardware backplane initialization
local modem = ppm.get_wireless_modem() if not backplane.init(config) then return end
if modem == nil then
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
databus.tx_hw_modem(true)
-- start UI -- 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 if not fp_ok then
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
@ -150,8 +144,7 @@ local function main()
local sv_facility = facility.new(config) local sv_facility = facility.new(config)
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) local superv_comms = supervisor.comms(SUPERVISOR_VERSION, fp_ok, sv_facility)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok, sv_facility)
-- base loop clock (6.67Hz, 3 ticks) -- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15 local MAIN_CLOCK = 0.15
@ -173,49 +166,13 @@ local function main()
-- handle event -- handle event
if event == "peripheral_detach" then if event == "peripheral_detach" then
local type, device = ppm.handle_unmount(param1) local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then backplane.detach(param1, type, device, println_ts)
---@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
end end
elseif event == "peripheral" then elseif event == "peripheral" then
local type, device = ppm.mount(param1) local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then backplane.attach(param1, type, device, println_ts)
---@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
end end
elseif event == "timer" and loop_clock.is_clock(param1) then elseif event == "timer" and loop_clock.is_clock(param1) then
-- main loop tick -- main loop tick

View File

@ -4,6 +4,8 @@ local util = require("scada-common.util")
local themes = require("graphics.themes") local themes = require("graphics.themes")
local backplane = require("supervisor.backplane")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local supervisor = {} local supervisor = {}
@ -11,6 +13,7 @@ local supervisor = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local PROBE_ACK = comms.PROBE_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
---@type svr_config ---@type svr_config
@ -58,6 +61,16 @@ function supervisor.load_config()
config.CRD_Timeout = settings.get("CRD_Timeout") config.CRD_Timeout = settings.get("CRD_Timeout")
config.PKT_Timeout = settings.get("PKT_Timeout") config.PKT_Timeout = settings.get("PKT_Timeout")
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.TrustedRange = settings.get("TrustedRange") config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey") config.AuthKey = settings.get("AuthKey")
@ -99,6 +112,19 @@ function supervisor.load_config()
cfv.assert_type_num(config.PKT_Timeout) cfv.assert_type_num(config.PKT_Timeout)
cfv.assert_min(config.PKT_Timeout, 2) cfv.assert_min(config.PKT_Timeout, 2)
cfv.assert_type_bool(config.WirelessModem)
cfv.assert((config.WiredModem == false) or (type(config.WiredModem) == "string"))
cfv.assert_type_num(config.PLC_Listen)
cfv.assert_range(config.PLC_Listen, 0, 2)
cfv.assert_type_num(config.RTU_Listen)
cfv.assert_range(config.RTU_Listen, 0, 2)
cfv.assert_type_num(config.CRD_Listen)
cfv.assert_range(config.CRD_Listen, 0, 2)
cfv.assert_type_bool(config.PocketEnabled)
cfv.assert_type_bool(config.PocketTest)
cfv.assert_type_num(config.TrustedRange) cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0) cfv.assert_min(config.TrustedRange, 0)
@ -123,36 +149,31 @@ end
-- supervisory controller communications -- supervisory controller communications
---@nodiscard ---@nodiscard
---@param _version string supervisor version ---@param _version string supervisor version
---@param nic nic network interface device
---@param fp_ok boolean if the front panel UI is running ---@param fp_ok boolean if the front panel UI is running
---@param facility facility facility instance ---@param facility facility facility instance
---@diagnostic disable-next-line: unused-local ---@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 -- 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 function println(message) if not fp_ok then util.println_ts(message) end end
local self = { local self = {
last_est_acks = {} last_est_acks = {} ---@type ESTABLISH_ACK[]
} }
comms.set_trusted_range(config.TrustedRange) comms.set_trusted_range(config.TrustedRange)
-- pass system data and objects to svsessions
svsessions.init(fp_ok, config, facility)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels
nic.closeAll()
nic.open(config.SVR_Channel)
-- pass system data and objects to svsessions
svsessions.init(nic, fp_ok, config, facility)
-- send an establish request response -- send an establish request response
---@param nic nic
---@param packet scada_packet ---@param packet scada_packet
---@param ack ESTABLISH_ACK ---@param ack ESTABLISH_ACK
---@param data? any optional data ---@param data? any optional data
local function _send_establish(packet, ack, data) local function _send_establish(nic, packet, ack, data)
local s_pkt = comms.scada_packet() local s_pkt, m_pkt = comms.scada_packet(), comms.mgmt_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data }) 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()) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
@ -161,6 +182,188 @@ function supervisor.comms(_version, nic, fp_ok, facility)
self.last_est_acks[packet.src_addr()] = ack self.last_est_acks[packet.src_addr()] = ack
end end
-- 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 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(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: 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: assignment collision with reactor ", reactor_id))
end
_send_establish(nic, 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, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("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 comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("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
-- 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: RTU_GW (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug("RTU_GW_ESTABLISH: packet length mismatch")
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
else
log.debug(util.c("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 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(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: CRD (", firmware_v, ") [@", src_addr, "] 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: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
end
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.COLLISION)
end
else
log.debug(util.c("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 comms_v ~= comms.version then
if last_ack ~= ESTABLISH_ACK.BAD_VERSION then
log.info(util.c("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: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id, " on ", nic.phy_name()))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.ALLOW)
else
log.debug(util.c("illegal establish packet for device ", dev_type, " on PKT channel"))
_send_establish(nic, packet.scada_frame, ESTABLISH_ACK.DENY)
end
end
--#endregion
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class superv_comms ---@class superv_comms
@ -175,36 +378,31 @@ function supervisor.comms(_version, nic, fp_ok, facility)
---@param distance integer ---@param distance integer
---@return modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil packet ---@return modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance) function public.parse_packet(side, sender, reply_to, message, distance)
local s_pkt = nic.receive(side, sender, reply_to, message, distance) local pkt, s_pkt, nic = nil, nil, backplane.nics[side]
local pkt = nil
if nic then
s_pkt = nic.receive(side, sender, reply_to, message, distance)
end
if s_pkt then if s_pkt then
-- get as MODBUS TCP packet -- get as MODBUS TCP packet
if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
local m_pkt = comms.modbus_packet() local m_pkt = comms.modbus_packet()
if m_pkt.decode(s_pkt) then if m_pkt.decode(s_pkt) then pkt = m_pkt.get() end
pkt = m_pkt.get()
end
-- get as RPLC packet -- get as RPLC packet
elseif s_pkt.protocol() == PROTOCOL.RPLC then elseif s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end
pkt = rplc_pkt.get()
end
-- get as SCADA management packet -- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end
pkt = mgmt_pkt.get()
end
-- get as coordinator packet -- get as coordinator packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet() local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then pkt = crdn_pkt.get() end
pkt = crdn_pkt.get()
end
else else
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) log.debug("receive[" .. side .. "] attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end end
end end
@ -214,6 +412,7 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- handle a packet -- handle a packet
---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame ---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame
function public.handle_packet(packet) function public.handle_packet(packet)
local nic = backplane.nics[packet.scada_frame.interface()]
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_channel() local r_chan = packet.scada_frame.remote_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
@ -226,81 +425,39 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- look for an associated session -- look for an associated session
local session = svsessions.find_plc_session(src_addr) local session = svsessions.find_plc_session(src_addr)
if protocol == PROTOCOL.RPLC then if session then
---@cast packet rplc_frame if nic ~= session.nic then
-- reactor PLC packet -- this is from the same device but on a different interface
if session ~= nil then -- 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 -- pass the packet onto the session handler
session.in_queue.push_packet(packet) 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 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 elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- SCADA management packet -- SCADA management packet
if session ~= nil then if packet.type == MGMT_TYPE.ESTABLISH then
-- pass the packet onto the session handler -- establish a new session: validate packet and continue
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.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1] _establish_plc(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
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
else else
log.debug("invalid establish packet (on PLC channel)") 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 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 else
-- any other packet should be session related, discard it -- 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)) log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr))
@ -312,62 +469,43 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- look for an associated session -- look for an associated session
local session = svsessions.find_rtu_session(src_addr) local session = svsessions.find_rtu_session(src_addr)
if protocol == PROTOCOL.MODBUS_TCP then if session then
---@cast packet modbus_frame if nic ~= session.nic then
-- MODBUS response -- this is from the same device but on a different interface
if session ~= nil then -- 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 -- pass the packet onto the session handler
session.in_queue.push_packet(packet) 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 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 elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- SCADA management packet -- SCADA management packet
if session ~= nil then if packet.type == MGMT_TYPE.ESTABLISH then
-- pass the packet onto the session handler -- establish a new session: validate packet and continue
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.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1] _establish_rtu_gw(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
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
else else
log.debug("invalid establish packet (on RTU channel)") 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 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 else
-- any other packet should be session related, discard it -- 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 end
else else
log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
@ -376,110 +514,64 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- look for an associated session -- look for an associated session
local session = svsessions.find_crd_session(src_addr) 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 ---@cast packet mgmt_frame
-- SCADA management packet log.debug(util.c("PROBE_ACK: conflict with CRD @", src_addr, " on ", session.nic.phy_name(), " probed on ", nic.phy_name()))
if session ~= nil then _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 -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then end
-- establish a new session elseif protocol == PROTOCOL.SCADA_MGMT then
local last_ack = self.last_est_acks[src_addr] ---@cast packet mgmt_frame
-- SCADA management packet
-- 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 if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1] _establish_crd(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
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
else else
log.debug("CRD_ESTABLISH: establish packet length mismatch") 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 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 else
-- any other packet should be session related, discard it -- 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)) log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr))
end end
elseif protocol == PROTOCOL.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@cast packet crdn_frame
-- coordinator packet -- coordinator packet, should be session related, discard it
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
log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr)) log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr))
end
else else
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) log.debug(util.c("illegal packet type ", protocol, " on CRD channel"))
end end
elseif r_chan == config.PKT_Channel then elseif r_chan == config.PKT_Channel then
-- look for an associated session -- look for an associated session
local session = svsessions.find_pdg_session(src_addr) local session = svsessions.find_pdg_session(src_addr)
if protocol == PROTOCOL.SCADA_MGMT then if session then
---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
elseif packet.type == MGMT_TYPE.ESTABLISH then elseif protocol == PROTOCOL.SCADA_MGMT then
-- establish a new session ---@cast packet mgmt_frame
local last_ack = self.last_est_acks[src_addr] -- SCADA management packet
if packet.type == MGMT_TYPE.ESTABLISH then
-- validate packet and continue -- 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 if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
local comms_v = packet.data[1] _establish_pdg(nic, packet, src_addr, i_seq_num, self.last_est_acks[src_addr])
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
else else
log.debug("PDG_ESTABLISH: establish packet length mismatch") 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 end
else else
-- any other packet should be session related, discard it -- any other packet should be session related, discard it
@ -487,14 +579,8 @@ function supervisor.comms(_version, nic, fp_ok, facility)
end end
elseif protocol == PROTOCOL.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@cast packet crdn_frame
-- coordinator packet -- coordinator packet, should be session related, discard it
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
log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr)) log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr))
end
else else
log.debug(util.c("illegal packet type ", protocol, " on pocket channel")) log.debug(util.c("illegal packet type ", protocol, " on pocket channel"))
end end