From d374967cb772c888e1ea02e90499b32632a9e6c3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 3 Apr 2025 22:56:54 -0400 Subject: [PATCH 01/42] #614 fixed reactor PLC self check when configured as not networked --- reactor-plc/config/check.lua | 9 ++++++--- reactor-plc/startup.lua | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/reactor-plc/config/check.lua b/reactor-plc/config/check.lua index 7a964fe..43f28ba 100644 --- a/reactor-plc/config/check.lua +++ b/reactor-plc/config/check.lua @@ -84,7 +84,7 @@ local function handle_packet(packet) elseif est_ack == ESTABLISH_ACK.COLLISION then error_msg = "another reactor PLC is connected with this reactor unit ID" elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update ...)" + error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update)" else error_msg = "error: invalid reply from supervisor" end @@ -124,7 +124,10 @@ local function self_check() local reactor = ppm.get_fission_reactor() local valid_cfg = plc.validate_config(self.settings) - self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC") + if self.settings.Networked then + self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC") + end + self.self_check_msg("> check fission reactor connected...", reactor ~= nil, "please connect the reactor PLC to the reactor's fission reactor logic adapter") self.self_check_msg("> check fission reactor formed...") -- this consumes events, but that is fine here @@ -132,7 +135,7 @@ local function self_check() self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones") - if valid_cfg and modem then + if self.settings.Networked and valid_cfg and modem then self.self_check_msg("> check supervisor connection...") -- init mac as needed diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 49d60a5..c085e95 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.8.19" +local R_PLC_VERSION = "v1.8.20" local println = util.println local println_ts = util.println_ts From c6a7de26692d0ab2ded14b6edab77520aa858bce Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 3 Apr 2025 23:06:45 -0400 Subject: [PATCH 02/42] #364 base RTU gateway self checks --- rtu/config/check.lua | 232 +++++++++++++++++++++++++++++++++++++++++++ rtu/configure.lua | 19 +++- rtu/rtu.lua | 48 +++++---- rtu/startup.lua | 2 +- 4 files changed, 275 insertions(+), 26 deletions(-) create mode 100644 rtu/config/check.lua diff --git a/rtu/config/check.lua b/rtu/config/check.lua new file mode 100644 index 0000000..861895d --- /dev/null +++ b/rtu/config/check.lua @@ -0,0 +1,232 @@ +local comms = require("scada-common.comms") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") + +local rtu = require("rtu.rtu") + +local core = require("graphics.core") + +local Div = require("graphics.elements.Div") +local ListBox = require("graphics.elements.ListBox") +local TextBox = require("graphics.elements.TextBox") + +local PushButton = require("graphics.elements.controls.PushButton") + +local tri = util.trinary + +local cpair = core.cpair + +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE +local ESTABLISH_ACK = comms.ESTABLISH_ACK +local MGMT_TYPE = comms.MGMT_TYPE + +local self = { + nic = nil, ---@type nic + net_listen = false, + sv_addr = comms.BROADCAST, + sv_seq_num = util.time_ms() * 10, + + self_check_pass = true, + + settings = nil, ---@type rtu_config + + run_test_btn = nil, ---@type PushButton + sc_log = nil, ---@type ListBox + self_check_msg = nil ---@type function +} + +-- report successful completion of the check +local function check_complete() + TextBox{parent=self.sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)} + TextBox{parent=self.sc_log,text=""} + local more = Div{parent=self.sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)} + TextBox{parent=more,text="if you still have a problem:"} + TextBox{parent=more,text="- check the wiki on GitHub"} + TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"} +end + +-- send a management packet to the supervisor +---@param msg_type MGMT_TYPE +---@param msg table +local function send_sv(msg_type, msg) + local s_pkt = comms.scada_packet() + local pkt = comms.mgmt_packet() + + pkt.make(msg_type, msg) + s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + + self.nic.transmit(self.settings.SVR_Channel, self.settings.RTU_Channel, s_pkt) + self.sv_seq_num = self.sv_seq_num + 1 +end + +-- handle an establish message from the supervisor +---@param packet mgmt_frame +local function handle_packet(packet) + local error_msg = nil + + if packet.scada_frame.local_channel() ~= self.settings.RTU_Channel then + error_msg = "error: unknown receive channel" + elseif packet.scada_frame.remote_channel() == self.settings.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then + if packet.type == MGMT_TYPE.ESTABLISH then + if packet.length == 1 then + local est_ack = packet.data[1] + + if est_ack== ESTABLISH_ACK.ALLOW then + self.self_check_msg(nil, true, "") + self.sv_addr = packet.scada_frame.src_addr() + send_sv(MGMT_TYPE.CLOSE, {}) + if self.self_check_pass then check_complete() end + elseif est_ack == ESTABLISH_ACK.DENY then + error_msg = "error: supervisor connection denied" + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + error_msg = "RTU gateway comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update)" + else + error_msg = "error: invalid reply from supervisor" + end + else + error_msg = "error: invalid reply length from supervisor" + end + else + error_msg = "error: didn't get an establish reply from supervisor" + end + end + + self.net_listen = false + self.run_test_btn.enable() + + if error_msg then + self.self_check_msg(nil, false, error_msg) + end +end + +-- handle supervisor connection failure +local function handle_timeout() + self.net_listen = false + self.run_test_btn.enable() + self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension") +end + +-- execute the self-check +local function self_check() + self.run_test_btn.disable() + + self.sc_log.remove_all() + ppm.mount_all() + + self.self_check_pass = true + + local modem = ppm.get_wireless_modem() + local valid_cfg = rtu.validate_config(self.settings) + + self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway") + + self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones") + + if valid_cfg and modem then + self.self_check_msg("> check supervisor connection...") + + -- init mac as needed + if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then + network.init_mac(self.settings.AuthKey) + else + network.deinit_mac() + end + + self.nic = network.nic(modem) + + self.nic.closeAll() + self.nic.open(self.settings.RTU_Channel) + + self.sv_addr = comms.BROADCAST + self.net_listen = true + + send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.RTU, {} }) + + tcd.dispatch_unique(8, handle_timeout) + else + if self.self_check_pass then check_complete() end + self.run_test_btn.enable() + end +end + +-- exit self check back home +---@param main_pane MultiPane +local function exit_self_check(main_pane) + tcd.abort(handle_timeout) + self.net_listen = false + self.run_test_btn.enable() + self.sc_log.remove_all() + main_pane.set_value(1) +end + +local check = {} + +-- create the self-check view +---@param main_pane MultiPane +---@param settings_cfg rtu_config +---@param check_sys Div +---@param style { [string]: cpair } +function check.create(main_pane, settings_cfg, check_sys, style) + local bw_fg_bg = style.bw_fg_bg + local g_lg_fg_bg = style.g_lg_fg_bg + local nav_fg_bg = style.nav_fg_bg + local btn_act_fg_bg = style.btn_act_fg_bg + local btn_dis_fg_bg = style.btn_dis_fg_bg + + self.settings = settings_cfg + + local sc = Div{parent=check_sys,x=2,y=4,width=49} + + TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg} + + self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local last_check = { nil, nil } + + function self.self_check_msg(msg, success, fail_msg) + if type(msg) == "string" then + last_check[1] = Div{parent=self.sc_log,height=1} + local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg} + last_check[2] = e.get_x()+string.len(msg) + end + + if type(fail_msg) == "string" then + TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))} + + if not success then + local fail = Div{parent=self.sc_log,height=#util.strwrap(fail_msg, 46)} + TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)} + end + + self.self_check_pass = self.self_check_pass and success + end + end + + PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} +end + +-- handle incoming modem messages +---@param side string +---@param sender integer +---@param reply_to integer +---@param message any +---@param distance integer +function check.receive_sv(side, sender, reply_to, message, distance) + if self.nic ~= nil and self.net_listen then + local s_pkt = self.nic.receive(side, sender, reply_to, message, distance) + + if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then + local mgmt_pkt = comms.mgmt_packet() + if mgmt_pkt.decode(s_pkt) then + tcd.abort(handle_timeout) + handle_packet(mgmt_pkt.get()) + end + end + end +end + +return check diff --git a/rtu/configure.lua b/rtu/configure.lua index 9b775a0..9db05bb 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -7,6 +7,7 @@ local ppm = require("scada-common.ppm") local tcd = require("scada-common.tcd") local util = require("scada-common.util") +local check = require("rtu.config.check") local peripherals = require("rtu.config.peripherals") local redstone = require("rtu.config.redstone") local system = require("rtu.config.system") @@ -169,8 +170,9 @@ local function config_view(display) local changelog = Div{parent=root_pane_div,x=1,y=1} local peri_cfg = Div{parent=root_pane_div,x=1,y=1} local rs_cfg = Div{parent=root_pane_div,x=1,y=1} + local check_sys = Div{parent=root_pane_div,x=1,y=1} - local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg}} + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg,check_sys}} --#region Main Page @@ -226,8 +228,9 @@ local function config_view(display) PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} local start_btn = PushButton{parent=main_page,x=42,y=17,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} - tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} - PushButton{parent=main_page,x=39,y=y_start+2,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=main_page,x=39,y=y_start,min_width=12,text="Self-Check",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start+2,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=main_page,x=39,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} if tool_ctl.ask_config then start_btn.disable() end @@ -283,6 +286,12 @@ local function config_view(display) PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} --#endregion + + --#region Self-Check + + check.create(main_pane, settings_cfg, check_sys, style) + + --#endregion end -- reset terminal screen @@ -317,7 +326,7 @@ function configurator.configure(ask_config) config_view(display) while true do - local event, param1, param2, param3 = util.pull_event() + local event, param1, param2, param3, param4, param5 = util.pull_event() -- handle event if event == "timer" then @@ -330,6 +339,8 @@ function configurator.configure(ask_config) if k_e then display.handle_key(k_e) end elseif event == "paste" then display.handle_paste(param1) + elseif event == "modem_message" then + check.receive_sv(param1, param2, param3, param4, param5) elseif event == "peripheral_detach" then ---@diagnostic disable-next-line: discard-returns ppm.handle_unmount(param1) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 2fce541..9c8e38b 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -46,36 +46,42 @@ function rtu.load_config() config.FrontPanelTheme = settings.get("FrontPanelTheme") config.ColorMode = settings.get("ColorMode") + return rtu.validate_config(config) +end + +-- validate a RTU gateway configuration +---@param cfg rtu_config +function rtu.validate_config(cfg) local cfv = util.new_validator() - cfv.assert_type_num(config.SpeakerVolume) - cfv.assert_range(config.SpeakerVolume, 0, 3) + cfv.assert_type_num(cfg.SpeakerVolume) + cfv.assert_range(cfg.SpeakerVolume, 0, 3) - cfv.assert_channel(config.SVR_Channel) - cfv.assert_channel(config.RTU_Channel) - cfv.assert_type_num(config.ConnTimeout) - cfv.assert_min(config.ConnTimeout, 2) - cfv.assert_type_num(config.TrustedRange) - cfv.assert_min(config.TrustedRange, 0) - cfv.assert_type_str(config.AuthKey) + cfv.assert_channel(cfg.SVR_Channel) + cfv.assert_channel(cfg.RTU_Channel) + cfv.assert_type_num(cfg.ConnTimeout) + cfv.assert_min(cfg.ConnTimeout, 2) + cfv.assert_type_num(cfg.TrustedRange) + cfv.assert_min(cfg.TrustedRange, 0) + cfv.assert_type_str(cfg.AuthKey) - if type(config.AuthKey) == "string" then - local len = string.len(config.AuthKey) + if type(cfg.AuthKey) == "string" then + local len = string.len(cfg.AuthKey) cfv.assert(len == 0 or len >= 8) end - cfv.assert_type_int(config.LogMode) - cfv.assert_range(config.LogMode, 0, 1) - cfv.assert_type_str(config.LogPath) - cfv.assert_type_bool(config.LogDebug) + cfv.assert_type_int(cfg.LogMode) + cfv.assert_range(cfg.LogMode, 0, 1) + cfv.assert_type_str(cfg.LogPath) + cfv.assert_type_bool(cfg.LogDebug) - cfv.assert_type_int(config.FrontPanelTheme) - cfv.assert_range(config.FrontPanelTheme, 1, 2) - cfv.assert_type_int(config.ColorMode) - cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES) + cfv.assert_type_int(cfg.FrontPanelTheme) + cfv.assert_range(cfg.FrontPanelTheme, 1, 2) + cfv.assert_type_int(cfg.ColorMode) + cfv.assert_range(cfg.ColorMode, 1, themes.COLOR_MODE.NUM_MODES) - cfv.assert_type_table(config.Peripherals) - cfv.assert_type_table(config.Redstone) + cfv.assert_type_table(cfg.Peripherals) + cfv.assert_type_table(cfg.Redstone) return cfv.valid() end diff --git a/rtu/startup.lua b/rtu/startup.lua index dfef1b7..01a05a9 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.11.6" +local RTU_VERSION = "v1.11.7" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_HW_STATE = databus.RTU_HW_STATE From ad834218c28d28532d11beb3e868ce63f130cb26 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 3 Apr 2025 23:21:06 -0400 Subject: [PATCH 03/42] #364 check all configured RTU peripherals in self check --- rtu/config/check.lua | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/rtu/config/check.lua b/rtu/config/check.lua index 861895d..d0ff05c 100644 --- a/rtu/config/check.lua +++ b/rtu/config/check.lua @@ -118,19 +118,25 @@ local function self_check() self.self_check_pass = true + local cfg = self.settings local modem = ppm.get_wireless_modem() - local valid_cfg = rtu.validate_config(self.settings) + local valid_cfg = rtu.validate_config(cfg) self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway") + self.self_check_msg("> check configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones (you may need to re-apply Peripheral and Redstone configurations as well)") - self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones") + -- check configured peripherals + for i = 1, #cfg.Peripherals do + local peri = cfg.Peripherals[i] + self.self_check_msg("> check " .. peri.name .. " connected...", ppm.get_device(peri.name), "please connect this device via a wired modem or direct contact and ensure the configuration matches what it connects as") + end if valid_cfg and modem then self.self_check_msg("> check supervisor connection...") -- init mac as needed - if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then - network.init_mac(self.settings.AuthKey) + if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then + network.init_mac(cfg.AuthKey) else network.deinit_mac() end @@ -138,7 +144,7 @@ local function self_check() self.nic = network.nic(modem) self.nic.closeAll() - self.nic.open(self.settings.RTU_Channel) + self.nic.open(cfg.RTU_Channel) self.sv_addr = comms.BROADCAST self.net_listen = true From cd654fb9b8e947e68601c7cfb7cfac9507200f47 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 3 Apr 2025 23:21:31 -0400 Subject: [PATCH 04/42] shorter variable for self.settings in PLC self check --- reactor-plc/config/check.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/reactor-plc/config/check.lua b/reactor-plc/config/check.lua index 43f28ba..91b71e6 100644 --- a/reactor-plc/config/check.lua +++ b/reactor-plc/config/check.lua @@ -120,11 +120,12 @@ local function self_check() self.self_check_pass = true + local cfg = self.settings local modem = ppm.get_wireless_modem() local reactor = ppm.get_fission_reactor() - local valid_cfg = plc.validate_config(self.settings) + local valid_cfg = plc.validate_config(cfg) - if self.settings.Networked then + if cfg.Networked then self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC") end @@ -135,12 +136,12 @@ local function self_check() self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones") - if self.settings.Networked and valid_cfg and modem then + if cfg.Networked and valid_cfg and modem then self.self_check_msg("> check supervisor connection...") -- init mac as needed - if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then - network.init_mac(self.settings.AuthKey) + if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then + network.init_mac(cfg.AuthKey) else network.deinit_mac() end @@ -148,12 +149,12 @@ local function self_check() self.nic = network.nic(modem) self.nic.closeAll() - self.nic.open(self.settings.PLC_Channel) + self.nic.open(cfg.PLC_Channel) self.sv_addr = comms.BROADCAST self.net_listen = true - send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, self.settings.UnitID }) + send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, cfg.UnitID }) tcd.dispatch_unique(8, handle_timeout) else From 2b3099ac59f73a7542159c82b3ee837d0c48bca4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 4 Apr 2025 00:17:40 -0400 Subject: [PATCH 05/42] #364 check validity of redstone and peripheral entries and check redstone side/color combos are not repeated --- rtu/config/check.lua | 59 ++++++++++++++++++++++++++++++++++++++--- rtu/config/redstone.lua | 9 +++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/rtu/config/check.lua b/rtu/config/check.lua index d0ff05c..ff3a2e3 100644 --- a/rtu/config/check.lua +++ b/rtu/config/check.lua @@ -1,11 +1,14 @@ local comms = require("scada-common.comms") local network = require("scada-common.network") local ppm = require("scada-common.ppm") +local rsio = require("scada-common.rsio") local tcd = require("scada-common.tcd") local util = require("scada-common.util") local rtu = require("rtu.rtu") +local redstone = require("rtu.config.redstone") + local core = require("graphics.core") local Div = require("graphics.elements.Div") @@ -109,6 +112,13 @@ local function handle_timeout() self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension") end + +-- check if a value is an integer within a range (inclusive) +---@param x any +---@param min integer +---@param max integer +local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end + -- execute the self-check local function self_check() self.run_test_btn.disable() @@ -123,12 +133,53 @@ local function self_check() local valid_cfg = rtu.validate_config(cfg) self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway") - self.self_check_msg("> check configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones (you may need to re-apply Peripheral and Redstone configurations as well)") + self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones") - -- check configured peripherals + -- check redstone configurations + local ifaces = {} + for i = 1, #cfg.Redstone do + local entry = cfg.Redstone[i] + local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "") + local dupe = util.table_contains(ifaces, ident) + + self.self_check_msg("> check redstone " .. ident .. " unique...", not dupe, "only one port should be set to a side/color combination") + self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry") + + table.insert(ifaces, ident) + end + + -- check peripheral configurations for i = 1, #cfg.Peripherals do - local peri = cfg.Peripherals[i] - self.self_check_msg("> check " .. peri.name .. " connected...", ppm.get_device(peri.name), "please connect this device via a wired modem or direct contact and ensure the configuration matches what it connects as") + local entry = cfg.Peripherals[i] + local valid = false + + if type(entry.name) == "string" then + self.self_check_msg("> check " .. entry.name .. " connected...", ppm.get_periph(entry.name), "please connect this device via a wired modem or direct contact and ensure the configuration matches what it connects as") + + local p_type = ppm.get_type(entry.name) + + if p_type == "boilerValve" then + valid = is_int_min_max(entry.index, 0, 2) and is_int_min_max(entry.unit, 1, 4) + elseif p_type == "turbineValve" then + valid = is_int_min_max(entry.index, 1, 3) and is_int_min_max(entry.unit, 1, 4) + elseif p_type == "solarNeutronActivator" then + valid = is_int_min_max(entry.unit, 1, 4) + elseif p_type == "dynamicValve" then + valid = (entry.unit == 0 and is_int_min_max(entry.index, 1, 4)) or is_int_min_max(entry.unit, 1, 4) + elseif p_type == "environmentDetector" then + valid = is_int_min_max(entry.unit, 0, 4) and util.is_int(entry.index) + else + valid = true + + if p_type ~= nil and not (p_type == "inductionPort" or p_type == "spsPort") then + self.self_check_msg("> check " .. entry.name .. " valid...", false, "unrecognized device type") + end + end + end + + if not valid then + self.self_check_msg("> check " .. entry.name .. " valid...", false, "configuration invalid, please re-configure peripheral entry") + end end if valid_cfg and modem then diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 2d9e9f5..2ec8cab 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -107,6 +107,15 @@ end local redstone = {} +-- validate a redstone entry +---@param def rtu_rs_definition +function redstone.validate(def) + return tri(PORT_DSGN[def.port] == 1, util.is_int(def.unit) and def.unit > 0 and def.unit <= 4, def.unit == nil) and + rsio.is_valid_port(def.port) and + rsio.is_valid_side(def.side) and + (def.color == nil or (rsio.is_digital(def.port) and rsio.is_color(def.color))) +end + -- create the redstone configuration view ---@param tool_ctl _rtu_cfg_tool_ctl ---@param main_pane MultiPane From b4a9366f73ff35fcec70a6a65afdee8b10df9298 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 5 Apr 2025 21:00:16 -0400 Subject: [PATCH 06/42] #364 fixes to redstone and peripheral checks --- rtu/config/check.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtu/config/check.lua b/rtu/config/check.lua index ff3a2e3..48d021f 100644 --- a/rtu/config/check.lua +++ b/rtu/config/check.lua @@ -159,15 +159,15 @@ local function self_check() local p_type = ppm.get_type(entry.name) if p_type == "boilerValve" then - valid = is_int_min_max(entry.index, 0, 2) and is_int_min_max(entry.unit, 1, 4) + valid = is_int_min_max(entry.index, 1, 2) and is_int_min_max(entry.unit, 1, 4) elseif p_type == "turbineValve" then valid = is_int_min_max(entry.index, 1, 3) and is_int_min_max(entry.unit, 1, 4) elseif p_type == "solarNeutronActivator" then valid = is_int_min_max(entry.unit, 1, 4) elseif p_type == "dynamicValve" then - valid = (entry.unit == 0 and is_int_min_max(entry.index, 1, 4)) or is_int_min_max(entry.unit, 1, 4) + valid = (entry.unit == nil and is_int_min_max(entry.index, 1, 4)) or is_int_min_max(entry.unit, 1, 4) elseif p_type == "environmentDetector" then - valid = is_int_min_max(entry.unit, 0, 4) and util.is_int(entry.index) + valid = (entry.unit == nil or is_int_min_max(entry.unit, 1, 4)) and util.is_int(entry.index) else valid = true From 1b692b5b9a888cccb1264927f10760c3666ab152 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 5 Apr 2025 21:14:33 -0400 Subject: [PATCH 07/42] #364 fail self check if using a side for both bundled and unbundled redstone --- rtu/config/check.lua | 6 ++++++ rtu/rtu.lua | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rtu/config/check.lua b/rtu/config/check.lua index 48d021f..2a77f39 100644 --- a/rtu/config/check.lua +++ b/rtu/config/check.lua @@ -137,14 +137,20 @@ local function self_check() -- check redstone configurations local ifaces = {} + local bundled_sides = {} for i = 1, #cfg.Redstone do local entry = cfg.Redstone[i] local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "") local dupe = util.table_contains(ifaces, ident) + local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil)) + + local mixed_msg = util.trinary(bundled_sides[entry.side], "this side has bundled entry(s) but this entry is not bundled", "this side has non-bundled entry(s) but this entry is bundled") self.self_check_msg("> check redstone " .. ident .. " unique...", not dupe, "only one port should be set to a side/color combination") + self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, mixed_msg .. ", which will not work") self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry") + bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil table.insert(ifaces, ident) end diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 9c8e38b..d7a576e 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -49,7 +49,7 @@ function rtu.load_config() return rtu.validate_config(config) end --- validate a RTU gateway configuration +-- validate an RTU gateway configuration ---@param cfg rtu_config function rtu.validate_config(cfg) local cfv = util.new_validator() From 0da944c3ea9da8ea44a80bf312a5422d676749a9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 5 Apr 2025 21:17:19 -0400 Subject: [PATCH 08/42] #364 updated scroll height max and reduced duplicate text --- rtu/config/check.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtu/config/check.lua b/rtu/config/check.lua index 2a77f39..6818ba6 100644 --- a/rtu/config/check.lua +++ b/rtu/config/check.lua @@ -144,10 +144,10 @@ local function self_check() local dupe = util.table_contains(ifaces, ident) local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil)) - local mixed_msg = util.trinary(bundled_sides[entry.side], "this side has bundled entry(s) but this entry is not bundled", "this side has non-bundled entry(s) but this entry is bundled") + local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is") self.self_check_msg("> check redstone " .. ident .. " unique...", not dupe, "only one port should be set to a side/color combination") - self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, mixed_msg .. ", which will not work") + self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work") self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry") bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil @@ -245,7 +245,7 @@ function check.create(main_pane, settings_cfg, check_sys, style) TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg} - self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=500,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local last_check = { nil, nil } From 97875f4e52b1debe88e28b10b47f3e30951fd98a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 19 Apr 2025 00:15:41 -0400 Subject: [PATCH 09/42] #583 graphical crash screens --- scada-common/crash.lua | 105 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/scada-common/crash.lua b/scada-common/crash.lua index a7e2c93..0b56626 100644 --- a/scada-common/crash.lua +++ b/scada-common/crash.lua @@ -2,6 +2,9 @@ -- Crash Handler -- +---@diagnostic disable-next-line: undefined-global +local _is_pocket_env = pocket -- luacheck: ignore pocket + local comms = require("scada-common.comms") local log = require("scada-common.log") local util = require("scada-common.util") @@ -36,6 +39,72 @@ local function log_versions(log_msg) if has_lockbox then log_msg(util.c("LOCKBOX VERSION: ", lockbox.version)) end end +-- render the standard computer crash screen +---@param exit function callback on exit button press +---@return DisplayBox display +local function draw_computer_crash(exit) + local DisplayBox = require("graphics.elements.DisplayBox") + local Div = require("graphics.elements.Div") + local Rectangle = require("graphics.elements.Rectangle") + local TextBox = require("graphics.elements.TextBox") + local PushButton = require("graphics.elements.controls.PushButton") + + local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)} + + local warning = Div{parent=display,x=2,y=2} + TextBox{parent=warning,x=7,text="\x90\n \x90\n \x90\n \x90\n \x90",fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,x=5,y=1,text="\x9f ",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=4,text="\x9f ",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=3,text="\x9f ",width=6,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=2,text="\x9f ",width=8,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,text="\x9f ",width=10,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,text="\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f",width=11,fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,x=6,y=3,text=" \n \x83",width=1,fg_bg=core.cpair(colors.yellow,colors.white)} + + TextBox{parent=display,x=13,y=2,text="Critical Software Fault Encountered",alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)} + TextBox{parent=display,x=15,y=4,text="Please consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER} + TextBox{parent=display,x=14,y=7,text="refer to the log file for more info",alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors._INHERIT)} + + local box = Rectangle{parent=display,x=2,y=9,width=display.get_width()-2,height=8,border=core.border(1,colors.gray,true),thin=true,fg_bg=core.cpair(colors.black,colors.white)} + TextBox{parent=box,text=err} + + PushButton{parent=display,x=23,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)} + + return display +end + +-- render the pocket crash screen +---@param exit function callback on exit button press +---@return DisplayBox display +local function draw_pocket_crash(exit) + local DisplayBox = require("graphics.elements.DisplayBox") + local Div = require("graphics.elements.Div") + local Rectangle = require("graphics.elements.Rectangle") + local TextBox = require("graphics.elements.TextBox") + local PushButton = require("graphics.elements.controls.PushButton") + + local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)} + + local warning = Div{parent=display,x=2,y=1} + TextBox{parent=warning,x=3,y=1,text="\x9f\x8b",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=2,text="\x9f \x8a",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,text="\x9f \x8a",width=6,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,text="\x8f\x8f\x8f\x8f\x8f\x8f\x85",width=7,fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,x=4,y=2,text="\x94",width=1,fg_bg=core.cpair(colors.white,colors.yellow)} + TextBox{parent=warning,x=4,y=3,text="\x91",width=1,fg_bg=core.cpair(colors.white,colors.yellow)} + + TextBox{parent=display,x=10,y=2,text=" Critical Software Fault",width=16,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)} + TextBox{parent=display,x=2,y=5,text="Consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER} + + local box = Rectangle{parent=display,y=9,width=display.get_width(),height=8,border=core.border(1,colors.gray,true),thin=true,fg_bg=core.cpair(colors.black,colors.white)} + TextBox{parent=box,text=err} + + PushButton{parent=display,x=11,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)} + TextBox{parent=display,x=2,y=20,text="see logs for details",width=24,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors._INHERIT)} + + return display +end + -- when running with debug logs, log the useful information that the crash handler knows function crash.dbg_log_env() log_versions(log.debug) end @@ -54,9 +123,41 @@ end -- final error print on failed xpcall, app exits here function crash.exit() + local handled, run = false, true + local display = nil ---@type DisplayBox + + -- special graphical crash screen + if has_graphics then + handled, display = pcall(util.trinary(_is_pocket_env, draw_pocket_crash, draw_computer_crash), function () run = false end) + + -- event loop + while display and run do + local event, param1, param2, param3 = util.pull_event() + + -- handle event + if event == "mouse_click" or event == "mouse_up" or event == "double_click" then + local mouse = core.events.new_mouse_event(event, param1, param2, param3) + if mouse then display.handle_mouse(mouse) end + elseif event == "terminate" then + break + end + end + + display.delete() + + term.setCursorPos(1, 1) + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + end + log.close() - util.println("fatal error occured in main application:") - error(err, 0) + + -- default text failure message + if not handled then + util.println("fatal error occured in main application:") + error(err, 0) + end end return crash From 592f1110edf8e142ad42905f49e3b458d0db82b8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 19 Apr 2025 00:16:43 -0400 Subject: [PATCH 10/42] define pocket global in craftos-pc environment pocket application --- pocket/startup.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pocket/startup.lua b/pocket/startup.lua index 4d72797..bf2416d 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -2,8 +2,10 @@ -- SCADA System Access on a Pocket Computer -- ----@diagnostic disable-next-line: undefined-global -local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket +---@diagnostic disable-next-line: undefined-global, lowercase-global +pocket = pocket or periphemu -- luacheck: ignore pocket + +local _is_pocket_env = pocket require("/initenv").init_env() From ae055a7d99214f812ef7369fcd84ff5441fd174f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 19 Apr 2025 00:21:15 -0400 Subject: [PATCH 11/42] luacheck fixes and version increment --- pocket/startup.lua | 4 ++-- scada-common/crash.lua | 2 +- scada-common/util.lua | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pocket/startup.lua b/pocket/startup.lua index bf2416d..c1d55ef 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -2,10 +2,10 @@ -- SCADA System Access on a Pocket Computer -- ----@diagnostic disable-next-line: undefined-global, lowercase-global +---@diagnostic disable-next-line: lowercase-global pocket = pocket or periphemu -- luacheck: ignore pocket -local _is_pocket_env = pocket +local _is_pocket_env = pocket -- luacheck: ignore pocket require("/initenv").init_env() diff --git a/scada-common/crash.lua b/scada-common/crash.lua index 0b56626..71256e1 100644 --- a/scada-common/crash.lua +++ b/scada-common/crash.lua @@ -124,7 +124,7 @@ end -- final error print on failed xpcall, app exits here function crash.exit() local handled, run = false, true - local display = nil ---@type DisplayBox + local display ---@type DisplayBox -- special graphical crash screen if has_graphics then diff --git a/scada-common/util.lua b/scada-common/util.lua index a4c7d4e..bdedb04 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -24,7 +24,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.4.12" +util.version = "1.5.0" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 From bfab2d6af20a1287f42da30420adc9026f4adbb0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 19 Apr 2025 18:09:43 -0400 Subject: [PATCH 12/42] #583 fixes for pocket crash display --- scada-common/crash.lua | 16 +++++++++------- scada-common/util.lua | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/scada-common/crash.lua b/scada-common/crash.lua index 71256e1..f5ace8a 100644 --- a/scada-common/crash.lua +++ b/scada-common/crash.lua @@ -86,17 +86,19 @@ local function draw_pocket_crash(exit) local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)} local warning = Div{parent=display,x=2,y=1} - TextBox{parent=warning,x=3,y=1,text="\x9f\x8b",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)} - TextBox{parent=warning,x=2,text="\x9f \x8a",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)} - TextBox{parent=warning,text="\x9f \x8a",width=6,fg_bg=core.cpair(colors.lightGray,colors.yellow)} - TextBox{parent=warning,text="\x8f\x8f\x8f\x8f\x8f\x8f\x85",width=7,fg_bg=core.cpair(colors.yellow,colors.lightGray)} - TextBox{parent=warning,x=4,y=2,text="\x94",width=1,fg_bg=core.cpair(colors.white,colors.yellow)} - TextBox{parent=warning,x=4,y=3,text="\x91",width=1,fg_bg=core.cpair(colors.white,colors.yellow)} + TextBox{parent=warning,x=4,y=1,text="\x90",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,x=3,text="\x81 ",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=5,y=2,text="\x94",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,x=2,text="\x81 ",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=6,y=3,text="\x94",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,text="\x8e\x8f\x8f\x8e\x8f\x8f\x84",width=7,fg_bg=core.cpair(colors.yellow,colors.lightGray)} + TextBox{parent=warning,x=4,y=2,text="\x90",width=1,fg_bg=core.cpair(colors.lightGray,colors.yellow)} + TextBox{parent=warning,x=4,y=3,text="\x85",width=1,fg_bg=core.cpair(colors.lightGray,colors.yellow)} TextBox{parent=display,x=10,y=2,text=" Critical Software Fault",width=16,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)} TextBox{parent=display,x=2,y=5,text="Consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER} - local box = Rectangle{parent=display,y=9,width=display.get_width(),height=8,border=core.border(1,colors.gray,true),thin=true,fg_bg=core.cpair(colors.black,colors.white)} + local box = Rectangle{parent=display,y=9,width=display.get_width(),height=8,fg_bg=core.cpair(colors.black,colors.white)} TextBox{parent=box,text=err} PushButton{parent=display,x=11,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)} diff --git a/scada-common/util.lua b/scada-common/util.lua index bdedb04..faa614c 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -24,7 +24,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.5.0" +util.version = "1.5.1" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 From f7c0a1d97dd5c2d04ec8fd83d3c7cb828fb0a456 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 20 Apr 2025 11:28:02 -0400 Subject: [PATCH 13/42] #484 work on redstone inversion --- rtu/config/redstone.lua | 1 + rtu/dev/redstone_rtu.lua | 87 ++++++++++++++++++++++++---------------- rtu/startup.lua | 2 +- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 2ec8cab..c7a7e19 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -20,6 +20,7 @@ local NumberField = require("graphics.elements.form.NumberField") ---@field port IO_PORT ---@field side side ---@field color color|nil +---@field invert true|nil local tri = util.trinary diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index c073250..8c3e5ee 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -33,16 +33,21 @@ function redstone_rtu.new() -- link digital input ---@param side string ---@param color integer - function public.link_di(side, color) + ---@param invert boolean|nil + function public.link_di(side, color, invert) local f_read ---@type function if color then - f_read = function () - return digital_read(rs.testBundledInput(side, color)) + if invert then + f_read = function () return digital_read(not rs.testBundledInput(side, color)) end + else + f_read = function () return digital_read(rs.testBundledInput(side, color)) end end else - f_read = function () - return digital_read(rs.getInput(side)) + if invert then + f_read = function () return digital_read(not rs.getInput(side)) end + else + f_read = function () return digital_read(rs.getInput(side)) end end end @@ -52,36 +57,58 @@ function redstone_rtu.new() -- link digital output ---@param side string ---@param color integer - function public.link_do(side, color) + ---@param invert boolean|nil + function public.link_do(side, color, invert) local f_read ---@type function local f_write ---@type function if color then - f_read = function () - return digital_read(colors.test(rs.getBundledOutput(side), color)) - end + if invert then + f_read = function () return digital_read(not colors.test(rs.getBundledOutput(side), color)) end - f_write = function (level) - if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then - local output = rs.getBundledOutput(side) + f_write = function (level) + if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then + local output = rs.getBundledOutput(side) - if digital_write(level) then - output = colors.combine(output, color) - else - output = colors.subtract(output, color) + -- inverted conditions + if digital_write(level) then + output = colors.subtract(output, color) + else output = colors.combine(output, color) end + + rs.setBundledOutput(side, output) end + end + else + f_read = function () return digital_read(colors.test(rs.getBundledOutput(side), color)) end - rs.setBundledOutput(side, output) + f_write = function (level) + if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then + local output = rs.getBundledOutput(side) + + if digital_write(level) then + output = colors.combine(output, color) + else output = colors.subtract(output, color) end + + rs.setBundledOutput(side, output) + end end end else - f_read = function () - return digital_read(rs.getOutput(side)) - end + if invert then + f_read = function () return digital_read(not rs.getOutput(side)) end - f_write = function (level) - if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then - rs.setOutput(side, digital_write(level)) + f_write = function (level) + if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then + rs.setOutput(side, not digital_write(level)) + end + end + else + f_read = function () return digital_read(rs.getOutput(side)) end + + f_write = function (level) + if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then + rs.setOutput(side, digital_write(level)) + end end end end @@ -92,23 +119,15 @@ function redstone_rtu.new() -- link analog input ---@param side string function public.link_ai(side) - unit.connect_input_reg( - function () - return rs.getAnalogInput(side) - end - ) + unit.connect_input_reg(function () return rs.getAnalogInput(side) end) end -- link analog output ---@param side string function public.link_ao(side) unit.connect_holding_reg( - function () - return rs.getAnalogOutput(side) - end, - function (value) - rs.setAnalogOutput(side, value) - end + function () return rs.getAnalogOutput(side) end, + function (value) rs.setAnalogOutput(side, value) end ) end diff --git a/rtu/startup.lua b/rtu/startup.lua index 01a05a9..686c6b4 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.11.7" +local RTU_VERSION = "v1.11.8" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_HW_STATE = databus.RTU_HW_STATE From d6e3a67562bd892554de03bcbff4269dbaaa2473 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 20 Apr 2025 17:36:16 -0400 Subject: [PATCH 14/42] #484 redstone inversion support --- rtu/config/redstone.lua | 49 +++++++++++++++++++++++++++++------------ rtu/configure.lua | 7 ++++-- rtu/startup.lua | 4 ++-- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index c7a7e19..973b172 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -42,7 +42,9 @@ local self = { rs_cfg_side_l = nil, ---@type TextBox rs_cfg_bundled = nil, ---@type Checkbox rs_cfg_color = nil, ---@type Radio2D - rs_cfg_shortcut = nil ---@type TextBox + rs_cfg_inverted = nil, ---@type Checkbox + rs_cfg_shortcut = nil, ---@type TextBox + rs_cfg_advanced = nil ---@type PushButton } -- rsio port descriptions @@ -142,8 +144,9 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}} + local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8}} TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} @@ -176,9 +179,6 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} - TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} - PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."} local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} @@ -201,6 +201,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) self.rs_cfg_color.hide(true) self.rs_cfg_shortcut.show() self.rs_cfg_side_l.set_value("Output Side") + self.rs_cfg_bundled.enable() + self.rs_cfg_advanced.disable() text = "You selected the ALL_WASTE shortcut." else self.rs_cfg_shortcut.hide(true) @@ -215,9 +217,12 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) self.rs_cfg_bundled.set_value(false) self.rs_cfg_bundled.disable() self.rs_cfg_color.disable() + self.rs_cfg_advanced.disable() else self.rs_cfg_bundled.enable() if self.rs_cfg_bundled.get_value() then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end + self.rs_cfg_inverted.set_value(false) + self.rs_cfg_advanced.enable() end if io_mode == IO_MODE.DIGITAL_IN then @@ -270,11 +275,6 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} - TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"} - TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"} - PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"} local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} @@ -313,7 +313,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) unit = tri(PORT_DSGN[port] == 1, u, nil), port = port, side = side_options_map[side.get_value()], - color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil) + color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil), + invert = self.rs_cfg_inverted.get_value() or nil } if self.rs_cfg_editing == false then @@ -342,10 +343,13 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) self.rs_cfg_bundled.set_value(false) self.rs_cfg_color.set_value(1) self.rs_cfg_color.disable() + self.rs_cfg_inverted.set_value(false) + self.rs_cfg_advanced.disable() else rs_err.show() end end PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.rs_cfg_advanced = PushButton{parent=rs_c_3,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(8)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"} @@ -356,6 +360,19 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} + PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} + TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"} + TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"} + PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_8,x=1,y=1,height=5,text="Advanced Options"} + self.rs_cfg_inverted = Checkbox{parent=rs_c_8,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg} + TextBox{parent=rs_c_8,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)} + PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + --#endregion --#region Tool Functions @@ -384,9 +401,11 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) if rsio.is_analog(def.port) then self.rs_cfg_bundled.set_value(false) self.rs_cfg_bundled.disable() + self.rs_cfg_advanced.disable() else self.rs_cfg_bundled.enable() self.rs_cfg_bundled.set_value(def.color ~= nil) + self.rs_cfg_advanced.enable() end local value = 1 @@ -401,6 +420,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) self.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) side.set_value(side_to_idx(def.side)) self.rs_cfg_color.set_value(value) + self.rs_cfg_inverted.set_value(def.invert or false) rs_pane.set_value(3) end @@ -419,14 +439,15 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) local def = tmp_cfg.Redstone[i] local name = rsio.to_string(def.port) - local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") + local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") + local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple) local conn = def.side local unit = util.strval(def.unit or "F") if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end local entry = Div{parent=rs_list,height=1} - TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} + TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)} TextBox{parent=entry,x=2,y=1,width=14,text=name} TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} @@ -437,7 +458,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) local a = ini_cfg.Redstone[i] local b = tmp_cfg.Redstone[i] - modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) + modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert) end end diff --git a/rtu/configure.lua b/rtu/configure.lua index 9db05bb..03fb1b9 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -35,7 +35,8 @@ local changes = { { "v1.7.9", { "ConnTimeout can now have a fractional part" } }, { "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }, - { "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } } + { "v1.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" } } } ---@class rtu_configurator @@ -116,6 +117,7 @@ local fields = { } -- deep copy peripherals defs +---@param data rtu_peri_definition[] function tool_ctl.deep_copy_peri(data) local array = {} for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end @@ -123,9 +125,10 @@ function tool_ctl.deep_copy_peri(data) end -- deep copy redstone defs +---@param data rtu_rs_definition[] function tool_ctl.deep_copy_rs(data) local array = {} - for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color }) end + for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color, invert = d.invert }) end return array end diff --git a/rtu/startup.lua b/rtu/startup.lua index 686c6b4..c1413a6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -197,10 +197,10 @@ local function main() println(message) log.warning(message) else - rs_rtu.link_di(entry.side, entry.color) + rs_rtu.link_di(entry.side, entry.color, entry.invert) end elseif mode == rsio.IO_MODE.DIGITAL_OUT then - rs_rtu.link_do(entry.side, entry.color) + rs_rtu.link_do(entry.side, entry.color, entry.invert) elseif mode == rsio.IO_MODE.ANALOG_IN then -- can't have duplicate inputs if util.table_contains(capabilities, entry.port) then From c8910bfc403d024c4fecce534efcf20c0aba115e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 20 Apr 2025 17:40:40 -0400 Subject: [PATCH 15/42] clear inverted on analog creation --- rtu/config/redstone.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 973b172..9826cb1 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -217,6 +217,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) self.rs_cfg_bundled.set_value(false) self.rs_cfg_bundled.disable() self.rs_cfg_color.disable() + self.rs_cfg_inverted.set_value(false) self.rs_cfg_advanced.disable() else self.rs_cfg_bundled.enable() From ee868eb6077ae6f3e8837bd7edfda4526ad57aa4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 20 Apr 2025 21:36:40 -0400 Subject: [PATCH 16/42] #616 updated flow view to match fluid colors not pellet colors --- coordinator/startup.lua | 2 +- coordinator/ui/components/unit_flow.lua | 10 +++++----- coordinator/ui/layout/flow_view.lua | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 3618e2b..5b89edc 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.6.11" +local COORDINATOR_VERSION = "v1.6.12" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/components/unit_flow.lua b/coordinator/ui/components/unit_flow.lua index eb00fa5..d8f3c0c 100644 --- a/coordinator/ui/components/unit_flow.lua +++ b/coordinator/ui/components/unit_flow.lua @@ -179,12 +179,12 @@ local function make(parent, x, y, wide, unit_id) pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true), pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true), - pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true), + pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.cyan, true), - pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true), - pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true), - pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true), - pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true), + pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.green, true), + pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true), + pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true), + pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, true), pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true), pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true), diff --git a/coordinator/ui/layout/flow_view.lua b/coordinator/ui/layout/flow_view.lua index 4de5e4d..2bdbbb7 100644 --- a/coordinator/ui/layout/flow_view.lua +++ b/coordinator/ui/layout/flow_view.lua @@ -268,7 +268,7 @@ local function init(main) for i = 1, facility.num_units do local y_offset = y_ofs(i) unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i) - table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true)) + table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true)) util.nop() end From 48ec973695c47e5fce18ebbe91e6c6204eb8c30b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 21 Apr 2025 22:13:58 -0400 Subject: [PATCH 17/42] #616 added pellet color option to the coordinator --- coordinator/config/hmi.lua | 9 ++++++--- coordinator/config/system.lua | 2 ++ coordinator/configure.lua | 5 ++++- coordinator/coordinator.lua | 2 ++ coordinator/startup.lua | 2 +- coordinator/ui/style.lua | 25 ++++++++++++++++--------- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/coordinator/config/hmi.lua b/coordinator/config/hmi.lua index c913da1..89daef6 100644 --- a/coordinator/config/hmi.lua +++ b/coordinator/config/hmi.lua @@ -234,16 +234,19 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style) TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)} - TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."} + TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."} TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"} tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"} + 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} + 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} - TextBox{parent=crd_c_1,x=24,y=8,text="Energy Scale"} - tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=24,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} + 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} local function submit_ui_opts() tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1 diff --git a/coordinator/config/system.lua b/coordinator/config/system.lua index bc78cb0..3bb035f 100644 --- a/coordinator/config/system.lua +++ b/coordinator/config/system.lua @@ -528,6 +528,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style) if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "GreenPuPellet" then + val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po") elseif f[1] == "TempScale" then val = util.strval(types.TEMP_SCALE_NAMES[raw]) elseif f[1] == "EnergyScale" then diff --git a/coordinator/configure.lua b/coordinator/configure.lua index a70f0b7..d76aac2 100644 --- a/coordinator/configure.lua +++ b/coordinator/configure.lua @@ -35,7 +35,8 @@ local changes = { { "v1.2.4", { "Added temperature scale options" } }, { "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } }, - { "v1.5.1", { "Added energy scale options" } } + { "v1.5.1", { "Added energy scale options" } }, + { "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } } } ---@class crd_configurator @@ -95,6 +96,7 @@ local tmp_cfg = { UnitCount = 1, SpeakerVolume = 1.0, Time24Hour = true, + GreenPuPellet = false, TempScale = 1, ---@type TEMP_SCALE EnergyScale = 1, ---@type ENERGY_SCALE DisableFlowView = false, @@ -129,6 +131,7 @@ local fields = { { "UnitDisplays", "Unit Monitors", {} }, { "SpeakerVolume", "Speaker Volume", 1.0 }, { "Time24Hour", "Use 24-hour Time Format", true }, + { "GreenPuPellet", "Pellet Colors", false }, { "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN }, { "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE }, { "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false }, diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index c0ba2f3..d5f930d 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -38,6 +38,7 @@ function coordinator.load_config() config.UnitCount = settings.get("UnitCount") config.SpeakerVolume = settings.get("SpeakerVolume") config.Time24Hour = settings.get("Time24Hour") + config.GreenPuPellet = settings.get("GreenPuPellet") config.TempScale = settings.get("TempScale") config.EnergyScale = settings.get("EnergyScale") @@ -67,6 +68,7 @@ function coordinator.load_config() cfv.assert_type_int(config.UnitCount) cfv.assert_range(config.UnitCount, 1, 4) cfv.assert_type_bool(config.Time24Hour) + cfv.assert_type_bool(config.GreenPuPellet) cfv.assert_type_int(config.TempScale) cfv.assert_range(config.TempScale, 1, 4) cfv.assert_type_int(config.EnergyScale) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 5b89edc..ee9468e 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.6.12" +local COORDINATOR_VERSION = "v1.6.13" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index 6f8f795..e22cb90 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -2,16 +2,20 @@ -- Graphics Style Options -- -local util = require("scada-common.util") +local util = require("scada-common.util") -local core = require("graphics.core") -local themes = require("graphics.themes") +local core = require("graphics.core") +local themes = require("graphics.themes") + +local coordinator = require("coordinator.coordinator") ---@class crd_style local style = {} local cpair = core.cpair +local config = coordinator.config + -- front panel styling style.fp_theme = themes.sandstone @@ -223,16 +227,19 @@ style.sps = { } } +local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan) +local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green) + style.waste = { -- auto waste processing states states = { - { color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, - { color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, + { color = cpair(colors.black, pu_color), text = "PLUTONIUM" }, + { color = cpair(colors.black, po_color), text = "POLONIUM" }, { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } }, states_abbrv = { - { color = cpair(colors.black, colors.green), text = "Pu" }, - { color = cpair(colors.black, colors.cyan), text = "Po" }, + { color = cpair(colors.black, pu_color), text = "Pu" }, + { color = cpair(colors.black, po_color), text = "Po" }, { color = cpair(colors.black, colors.purple), text = "AM" } }, -- process radio button options @@ -240,8 +247,8 @@ style.waste = { -- unit waste selection unit_opts = { { text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) }, - { text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.green) }, - { text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.cyan) }, + { text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) }, + { text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) }, { text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) } } } From 0d7302dc8ea4c4e600fe16cdebf60307c684e86b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 21 Apr 2025 22:18:09 -0400 Subject: [PATCH 18/42] #604 start of total rework of redstone RTUs for relay functionalitiy --- rtu/config/redstone.lua | 1 + rtu/dev/redstone_rtu.lua | 12 ++++++- rtu/modbus.lua | 46 +++++++++++++-------------- rtu/rtu.lua | 10 ++++-- rtu/startup.lua | 69 +++++++++++++++++++++++++--------------- rtu/threads.lua | 2 ++ 6 files changed, 87 insertions(+), 53 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 9826cb1..854d6b9 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -18,6 +18,7 @@ local NumberField = require("graphics.elements.form.NumberField") ---@class rtu_rs_definition ---@field unit integer|nil ---@field port IO_PORT +---@field relay string|nil ---@field side side ---@field color color|nil ---@field invert true|nil diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 8c3e5ee..366e960 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -11,10 +11,14 @@ local digital_write = rsio.digital_write -- create new redstone device ---@nodiscard +---@param relay? table optional redstone relay to use instead of the computer's redstone interface ---@return rtu_rs_device interface, boolean faulted -function redstone_rtu.new() +function redstone_rtu.new(relay) local unit = rtu.init_unit() + -- physical interface to use + local phy = relay or rs + -- get RTU interface local interface = unit.interface() @@ -30,6 +34,12 @@ function redstone_rtu.new() write_holding_reg = interface.write_holding_reg } + -- change the phy in use (a relay or rs) + ---@param new_phy table + function public.change_phy(new_phy) phy = new_phy end + + -- NOTE: for runtime speed, inversion logic results in extra code here but less code when functions are called + -- link digital input ---@param side string ---@param color integer diff --git a/rtu/modbus.lua b/rtu/modbus.lua index d55907f..ed8ee19 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -399,43 +399,41 @@ function modbus.new(rtu_dev, use_parallel_read) return public end +-- create an error reply +---@nodiscard +---@param packet modbus_frame MODBUS packet frame +---@param code MODBUS_EXCODE exception code +---@return modbus_packet reply +local function excode_reply(packet, code) + -- reply back with error flag and exception code + local reply = comms.modbus_packet() + local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + reply.make(packet.txn_id, packet.unit_id, fcode, { code }) + return reply +end + +-- return a SERVER_DEVICE_FAIL error reply +---@nodiscard +---@param packet modbus_frame MODBUS packet frame +---@return modbus_packet reply +function modbus.reply__srv_device_fail(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end + -- return a SERVER_DEVICE_BUSY error reply ---@nodiscard ---@param packet modbus_frame MODBUS packet frame ---@return modbus_packet reply -function modbus.reply__srv_device_busy(packet) - -- reply back with error flag and exception code - local reply = comms.modbus_packet() - local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) - local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY } - reply.make(packet.txn_id, packet.unit_id, fcode, data) - return reply -end +function modbus.reply__srv_device_busy(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end -- return a NEG_ACKNOWLEDGE error reply ---@nodiscard ---@param packet modbus_frame MODBUS packet frame ---@return modbus_packet reply -function modbus.reply__neg_ack(packet) - -- reply back with error flag and exception code - local reply = comms.modbus_packet() - local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) - local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE } - reply.make(packet.txn_id, packet.unit_id, fcode, data) - return reply -end +function modbus.reply__neg_ack(packet) return excode_reply(packet, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end -- return a GATEWAY_PATH_UNAVAILABLE error reply ---@nodiscard ---@param packet modbus_frame MODBUS packet frame ---@return modbus_packet reply -function modbus.reply__gw_unavailable(packet) - -- reply back with error flag and exception code - local reply = comms.modbus_packet() - local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) - local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE } - reply.make(packet.txn_id, packet.unit_id, fcode, data) - return reply -end +function modbus.reply__gw_unavailable(packet) return excode_reply(packet, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end return modbus diff --git a/rtu/rtu.lua b/rtu/rtu.lua index d7a576e..c7401fe 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -477,9 +477,15 @@ function rtu.comms(version, nic, conn_watchdog) local unit = units[packet.unit_id] local unit_dbg_tag = " (unit " .. packet.unit_id .. ")" - if unit.name == "redstone_io" then + if unit.type == RTU_UNIT_TYPE.REDSTONE then -- immediately execute redstone RTU requests - return_code, reply = unit.modbus_io.handle_packet(packet) + if not unit.device then + reply = modbus.reply__srv_device_fail(packet) + return_code = false + else + return_code, reply = unit.modbus_io.handle_packet(packet) + end + if not return_code then log.warning("requested MODBUS operation failed" .. unit_dbg_tag) end diff --git a/rtu/startup.lua b/rtu/startup.lua index c1413a6..d875ae6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -143,29 +143,23 @@ local function main() -- configure RTU gateway based on settings file definitions local function sys_config() -- redstone interfaces - local rs_rtus = {} ---@type { rtu: rtu_rs_device, capabilities: IO_PORT[] }[] + local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table|nil, banks: IO_PORT[][] }[] -- go through redstone definitions list for entry_idx = 1, #rtu_redstone do local entry = rtu_redstone[entry_idx] + local assignment local for_reactor = entry.unit - local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) + local phy = entry.relay or 0 + local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then ---@cast for_reactor integer assignment = "reactor unit " .. entry.unit - if rs_rtus[for_reactor] == nil then - log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit)) - rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} } - end elseif entry.unit == nil then assignment = "facility" for_reactor = 0 - if rs_rtus[for_reactor] == nil then - log.debug(util.c("sys_config> allocated redstone RTU for the facility")) - rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} } - end else local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx) println(message) @@ -173,6 +167,31 @@ local function main() return false end + -- create the appropriate RTU if it doesn't exist and check relay name validity + if entry.relay then + if type(entry.relay) ~= "string" then + local message = util.c("sys_config> invalid redstone relay '", entry.relay, '"') + println(message) + log.fatal(message) + return false + elseif not rs_rtus[entry.relay] then + log.debug(util.c("sys_config> allocated relay redstone RTU for interface ", entry.relay)) + + local relay = ppm.get_device(entry.relay) + + if not relay then + log.warning(util.c("sys_config> redstone relay ", entry.relay, " not connected")) + elseif ppm.get_type(entry.relay) ~= "redstone_relay" then + log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay")) + end + + rs_rtus[entry.relay] = { name = entry.relay, rtu = redstone_rtu.new(relay), phy = relay, banks = {} } + end + elseif rs_rtus[0] == nil then + log.debug(util.c("sys_config> allocated local redstone RTU")) + rs_rtus[0] = { name = "redstone_local", rtu = redstone_rtu.new(), phy = rs, banks = {} } + end + -- verify configuration local valid = false if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then @@ -180,7 +199,7 @@ local function main() end local rs_rtu = rs_rtus[for_reactor].rtu - local capabilities = rs_rtus[for_reactor].capabilities + local conns = rs_rtus[phy].banks[for_reactor] if not valid then local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx) @@ -192,7 +211,7 @@ local function main() local mode = rsio.get_io_mode(entry.port) if mode == rsio.IO_MODE.DIGITAL_IN then -- can't have duplicate inputs - if util.table_contains(capabilities, entry.port) then + 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) println(message) log.warning(message) @@ -203,7 +222,7 @@ local function main() rs_rtu.link_do(entry.side, entry.color, entry.invert) elseif mode == rsio.IO_MODE.ANALOG_IN then -- can't have duplicate inputs - if util.table_contains(capabilities, entry.port) then + 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) println(message) log.warning(message) @@ -219,25 +238,28 @@ local function main() return false end - table.insert(capabilities, entry.port) + table.insert(conns, entry.port) - log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment)) + log.debug(util.c("sys_config> linked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment)) end end -- create unit entries for redstone RTUs - for for_reactor, def in pairs(rs_rtus) do + for _, def in pairs(rs_rtus) do + local hw_state = util.trinary(def.phy, RTU_HW_STATE.OK, RTU_HW_STATE.OFFLINE) + ---@class rtu_registry_entry local unit = { uid = 0, ---@type integer - name = "redstone_io", ---@type string + name = def.name, ---@type string type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE index = false, ---@type integer|false - reactor = for_reactor, ---@type integer - device = def.capabilities, ---@type IO_PORT[] use device field for redstone ports + reactor = nil, ---@type nil + device = def.phy, ---@type table|nil + banks = def.banks, ---@type IO_PORT[][] is_multiblock = false, ---@type boolean formed = nil, ---@type boolean|nil - hw_state = RTU_HW_STATE.OK, ---@type RTU_HW_STATE + hw_state = hw_state, ---@type RTU_HW_STATE rtu = def.rtu, ---@type rtu_device|rtu_rs_device modbus_io = modbus.new(def.rtu, false), pkt_queue = nil, ---@type mqueue|nil @@ -246,12 +268,7 @@ local function main() table.insert(units, unit) - local for_message = "facility" - if util.is_int(for_reactor) then - for_message = util.c("reactor unit ", for_reactor) - end - - log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message)) + log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (redstone)")) unit.uid = #units diff --git a/rtu/threads.lua b/rtu/threads.lua index e1d8c6c..7b872bf 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -132,6 +132,8 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit) unit.rtu, faulted = sna_rtu.new(device) elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then unit.rtu, faulted = envd_rtu.new(device) + elseif unit.type == RTU_UNIT_TYPE.REDSTONE then + unit.rtu.change_phy(device) else unknown = true log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) From 1af2cdba8d97d59d757bd12fd031b82ddfe19f53 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 26 Apr 2025 15:21:09 -0400 Subject: [PATCH 19/42] #616 fixes to coordinator pellet color option --- coordinator/config/hmi.lua | 1 + coordinator/config/system.lua | 1 + coordinator/configure.lua | 1 + coordinator/startup.lua | 2 +- coordinator/ui/components/process_ctl.lua | 6 +-- coordinator/ui/components/unit_detail.lua | 2 +- coordinator/ui/style.lua | 50 ++++++++++++----------- 7 files changed, 35 insertions(+), 28 deletions(-) diff --git a/coordinator/config/hmi.lua b/coordinator/config/hmi.lua index 89daef6..80c67fe 100644 --- a/coordinator/config/hmi.lua +++ b/coordinator/config/hmi.lua @@ -250,6 +250,7 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style) local function submit_ui_opts() tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1 + tmp_cfg.GreenPuPellet = tool_ctl.pellet_color.get_value() == 1 tmp_cfg.TempScale = tool_ctl.temp_scale.get_value() tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value() main_pane.set_value(7) diff --git a/coordinator/config/system.lua b/coordinator/config/system.lua index 3bb035f..5becf55 100644 --- a/coordinator/config/system.lua +++ b/coordinator/config/system.lua @@ -380,6 +380,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style) try_set(tool_ctl.num_units, ini_cfg.UnitCount) try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView) try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume) + try_set(tool_ctl.pellet_color, ini_cfg.GreenPuPellet) try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2)) try_set(tool_ctl.temp_scale, ini_cfg.TempScale) try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale) diff --git a/coordinator/configure.lua b/coordinator/configure.lua index d76aac2..474c727 100644 --- a/coordinator/configure.lua +++ b/coordinator/configure.lua @@ -78,6 +78,7 @@ local tool_ctl = { -- settings elements from hmi dis_flow_view = nil, ---@type Checkbox s_vol = nil, ---@type NumberField + pellet_color = nil, ---@type RadioButton clock_fmt = nil, ---@type RadioButton temp_scale = nil, ---@type RadioButton energy_scale = nil, ---@type RadioButton diff --git a/coordinator/startup.lua b/coordinator/startup.lua index ee9468e..323af98 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.6.13" +local COORDINATOR_VERSION = "v1.6.14" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index f649ac5..780905e 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -325,7 +325,7 @@ local function new_view(root, x, y) TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8} local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht} - local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6} + local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6} a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update) waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update) @@ -339,11 +339,11 @@ local function new_view(root, x, y) TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg} local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} - local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} + local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17} status.register(facility.ps, "current_waste_product", status.update) - local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown} + local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown} waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 8c9ce7a..44d082c 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -398,7 +398,7 @@ local function init(parent, id) local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} - local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6} + local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6} waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua index e22cb90..2aff611 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -227,30 +227,34 @@ style.sps = { } } -local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan) -local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green) +-- get waste styling, which depends on the configuration +---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: { text: string, fg_bg: cpair, active_fg_bg:cpair } } +function style.get_waste() + local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan) + local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green) -style.waste = { - -- auto waste processing states - states = { - { color = cpair(colors.black, pu_color), text = "PLUTONIUM" }, - { color = cpair(colors.black, po_color), text = "POLONIUM" }, - { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } - }, - states_abbrv = { - { color = cpair(colors.black, pu_color), text = "Pu" }, - { color = cpair(colors.black, po_color), text = "Po" }, - { color = cpair(colors.black, colors.purple), text = "AM" } - }, - -- process radio button options - options = { "Plutonium", "Polonium", "Antimatter" }, - -- unit waste selection - unit_opts = { - { text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) }, - { text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) }, - { text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) }, - { text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) } + return { + -- auto waste processing states + states = { + { color = cpair(colors.black, pu_color), text = "PLUTONIUM" }, + { color = cpair(colors.black, po_color), text = "POLONIUM" }, + { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } + }, + states_abbrv = { + { color = cpair(colors.black, pu_color), text = "Pu" }, + { color = cpair(colors.black, po_color), text = "Po" }, + { color = cpair(colors.black, colors.purple), text = "AM" } + }, + -- process radio button options + options = { "Plutonium", "Polonium", "Antimatter" }, + -- unit waste selection + unit_opts = { + { text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) }, + { text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) }, + { text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) }, + { text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) } + } } -} +end return style From 04c53c7074b652600e675220de5b61cefa3304b2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 26 Apr 2025 15:24:50 -0400 Subject: [PATCH 20/42] #616 pocket pellet color options --- pocket/config/system.lua | 35 ++++++++++++++++++++++------ pocket/configure.lua | 5 +++- pocket/pocket.lua | 2 ++ pocket/startup.lua | 2 +- pocket/ui/apps/waste.lua | 8 +++---- pocket/ui/style.lua | 49 +++++++++++++++++++++++++--------------- 6 files changed, 70 insertions(+), 31 deletions(-) diff --git a/pocket/config/system.lua b/pocket/config/system.lua index 321ef59..110a6cf 100644 --- a/pocket/config/system.lua +++ b/pocket/config/system.lua @@ -53,25 +53,43 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) --#region Pocket UI local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24} + local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24} + + local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}} TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)} - TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."} + TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."} - TextBox{parent=ui_c_1,x=1,y=4,text="Temperature Scale"} - local temp_scale = RadioButton{parent=ui_c_1,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} + TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"} + 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} - TextBox{parent=ui_c_1,x=1,y=10,text="Energy Scale"} - local energy_scale = RadioButton{parent=ui_c_1,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} + 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} local function submit_ui_opts() + tmp_cfg.GreenPuPellet = pellet_color.get_value() == 1 + ui_pane.set_value(2) + end + + PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."} + + TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"} + local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + 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 function submit_ui_units() tmp_cfg.TempScale = temp_scale.get_value() tmp_cfg.EnergyScale = energy_scale.get_value() main_pane.set_value(3) end - PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} --#endregion @@ -266,6 +284,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) load_settings(settings_cfg, true) load_settings(ini_cfg) + try_set(pellet_color, ini_cfg.GreenPuPellet) try_set(temp_scale, ini_cfg.TempScale) try_set(energy_scale, ini_cfg.EnergyScale) try_set(svr_chan, ini_cfg.SVR_Channel) @@ -374,6 +393,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) val = string.rep("*", string.len(val)) elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "GreenPuPellet" then + val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po") elseif f[1] == "TempScale" then val = util.strval(types.TEMP_SCALE_NAMES[raw]) elseif f[1] == "EnergyScale" then diff --git a/pocket/configure.lua b/pocket/configure.lua index 175be03..d714d9f 100644 --- a/pocket/configure.lua +++ b/pocket/configure.lua @@ -29,7 +29,8 @@ local CENTER = core.ALIGN.CENTER -- changes to the config data/format to let the user know local changes = { { "v0.9.2", { "Added temperature scale options" } }, - { "v0.11.3", { "Added energy scale options" } } + { "v0.11.3", { "Added energy scale options" } }, + { "v0.13.2", { "Added option for Po/Pu pellet green/cyan pairing" } } } ---@class pkt_configurator @@ -64,6 +65,7 @@ local tool_ctl = { ---@class pkt_config local tmp_cfg = { + GreenPuPellet = false, TempScale = 1, ---@type TEMP_SCALE EnergyScale = 1, ---@type ENERGY_SCALE SVR_Channel = nil, ---@type integer @@ -84,6 +86,7 @@ local settings_cfg = {} -- all settings fields, their nice names, and their default values local fields = { + { "GreenPuPellet", "Pellet Colors", false }, { "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN }, { "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE }, { "SVR_Channel", "SVR Channel", 16240 }, diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 7bfe669..e0e285c 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -38,6 +38,7 @@ pocket.config = config function pocket.load_config() if not settings.load("/pocket.settings") then return false end + config.GreenPuPellet = settings.get("GreenPuPellet") config.TempScale = settings.get("TempScale") config.EnergyScale = settings.get("EnergyScale") @@ -54,6 +55,7 @@ function pocket.load_config() local cfv = util.new_validator() + cfv.assert_type_bool(config.GreenPuPellet) cfv.assert_type_int(config.TempScale) cfv.assert_range(config.TempScale, 1, 4) cfv.assert_type_int(config.EnergyScale) diff --git a/pocket/startup.lua b/pocket/startup.lua index c1d55ef..a2eddd6 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -22,7 +22,7 @@ local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") local threads = require("pocket.threads") -local POCKET_VERSION = "v0.13.1-beta" +local POCKET_VERSION = "v0.13.2-beta" local println = util.println local println_ts = util.println_ts diff --git a/pocket/ui/apps/waste.lua b/pocket/ui/apps/waste.lua index 8037d82..bd39e7c 100644 --- a/pocket/ui/apps/waste.lua +++ b/pocket/ui/apps/waste.lua @@ -95,8 +95,8 @@ local function new_view(root) local function set_waste(mode) process.set_unit_waste(i, mode) end - local waste_prod = StateIndicator{parent=u_div,x=16,y=3,states=style.waste.states_abbrv,value=1,min_width=6} - local waste_mode = RadioButton{parent=u_div,y=3,options=style.waste.unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white} + local waste_prod = StateIndicator{parent=u_div,x=16,y=3,states=style.get_waste().states_abbrv,value=1,min_width=6} + local waste_mode = RadioButton{parent=u_div,y=3,options=style.get_waste().unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white} waste_prod.register(u_ps, "U_WasteProduct", waste_prod.update) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) @@ -159,8 +159,8 @@ local function new_view(root) TextBox{parent=c_div,y=1,text="Waste Control",alignment=ALIGN.CENTER} - local status = StateIndicator{parent=c_div,x=3,y=3,states=style.waste.states,value=1,min_width=17} - local waste_prod = RadioButton{parent=c_div,y=5,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white} + local status = StateIndicator{parent=c_div,x=3,y=3,states=style.get_waste().states,value=1,min_width=17} + local waste_prod = RadioButton{parent=c_div,y=5,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white} status.register(f_ps, "current_waste_product", status.update) waste_prod.register(f_ps, "process_waste_product", waste_prod.set_value) diff --git a/pocket/ui/style.lua b/pocket/ui/style.lua index 7b35cc4..0c77806 100644 --- a/pocket/ui/style.lua +++ b/pocket/ui/style.lua @@ -2,12 +2,18 @@ -- Graphics Style Options -- -local core = require("graphics.core") +local util = require("scada-common.util") + +local core = require("graphics.core") + +local pocket = require("pocket.pocket") local style = {} local cpair = core.cpair +local config = pocket.config + -- GLOBAL -- style.root = cpair(colors.white, colors.black) @@ -171,22 +177,29 @@ style.sps = { } } -style.waste = { - -- auto waste processing states - states = { - { color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, - { color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, - { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } - }, - states_abbrv = { - { color = cpair(colors.black, colors.green), text = "Pu" }, - { color = cpair(colors.black, colors.cyan), text = "Po" }, - { color = cpair(colors.black, colors.purple), text = "AM" } - }, - -- process radio button options - options = { "Plutonium", "Polonium", "Antimatter" }, - -- unit waste selection - unit_opts = { "Auto", "Plutonium", "Polonium", "Antimatter" } -} +-- get waste styling, which depends on the configuration +---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: string[] } +function style.get_waste() + local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan) + local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green) + + return { + -- auto waste processing states + states = { + { color = cpair(colors.black, pu_color), text = "PLUTONIUM" }, + { color = cpair(colors.black, po_color), text = "POLONIUM" }, + { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } + }, + states_abbrv = { + { color = cpair(colors.black, pu_color), text = "Pu" }, + { color = cpair(colors.black, po_color), text = "Po" }, + { color = cpair(colors.black, colors.purple), text = "AM" } + }, + -- process radio button options + options = { "Plutonium", "Polonium", "Antimatter" }, + -- unit waste selection + unit_opts = { "Auto", "Plutonium", "Polonium", "Antimatter" } + } +end return style From 1dc3d82e59336c7824558c7ac7f5b9ba15dca571 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 27 Apr 2025 22:40:42 -0400 Subject: [PATCH 21/42] #604 work on redstone RTU rework --- rtu/dev/redstone_rtu.lua | 36 ++++++++++----------- rtu/startup.lua | 68 ++++++++++++++++++++++++++++++++-------- rtu/threads.lua | 2 +- 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 366e960..5772a10 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -36,7 +36,7 @@ function redstone_rtu.new(relay) -- change the phy in use (a relay or rs) ---@param new_phy table - function public.change_phy(new_phy) phy = new_phy end + function public.remount_phy(new_phy) phy = new_phy end -- NOTE: for runtime speed, inversion logic results in extra code here but less code when functions are called @@ -49,15 +49,15 @@ function redstone_rtu.new(relay) if color then if invert then - f_read = function () return digital_read(not rs.testBundledInput(side, color)) end + f_read = function () return digital_read(not phy.testBundledInput(side, color)) end else - f_read = function () return digital_read(rs.testBundledInput(side, color)) end + f_read = function () return digital_read(phy.testBundledInput(side, color)) end end else if invert then - f_read = function () return digital_read(not rs.getInput(side)) end + f_read = function () return digital_read(not phy.getInput(side)) end else - f_read = function () return digital_read(rs.getInput(side)) end + f_read = function () return digital_read(phy.getInput(side)) end end end @@ -74,50 +74,50 @@ function redstone_rtu.new(relay) if color then if invert then - f_read = function () return digital_read(not colors.test(rs.getBundledOutput(side), color)) end + f_read = function () return digital_read(not colors.test(phy.getBundledOutput(side), color)) end f_write = function (level) if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then - local output = rs.getBundledOutput(side) + local output = phy.getBundledOutput(side) -- inverted conditions if digital_write(level) then output = colors.subtract(output, color) else output = colors.combine(output, color) end - rs.setBundledOutput(side, output) + phy.setBundledOutput(side, output) end end else - f_read = function () return digital_read(colors.test(rs.getBundledOutput(side), color)) end + f_read = function () return digital_read(colors.test(phy.getBundledOutput(side), color)) end f_write = function (level) if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then - local output = rs.getBundledOutput(side) + local output = phy.getBundledOutput(side) if digital_write(level) then output = colors.combine(output, color) else output = colors.subtract(output, color) end - rs.setBundledOutput(side, output) + phy.setBundledOutput(side, output) end end end else if invert then - f_read = function () return digital_read(not rs.getOutput(side)) end + f_read = function () return digital_read(not phy.getOutput(side)) end f_write = function (level) if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then - rs.setOutput(side, not digital_write(level)) + phy.setOutput(side, not digital_write(level)) end end else - f_read = function () return digital_read(rs.getOutput(side)) end + f_read = function () return digital_read(phy.getOutput(side)) end f_write = function (level) if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then - rs.setOutput(side, digital_write(level)) + phy.setOutput(side, digital_write(level)) end end end @@ -129,15 +129,15 @@ function redstone_rtu.new(relay) -- link analog input ---@param side string function public.link_ai(side) - unit.connect_input_reg(function () return rs.getAnalogInput(side) end) + unit.connect_input_reg(function () return phy.getAnalogInput(side) end) end -- link analog output ---@param side string function public.link_ao(side) unit.connect_holding_reg( - function () return rs.getAnalogOutput(side) end, - function (value) rs.setAnalogOutput(side, value) end + function () return phy.getAnalogOutput(side) end, + function (value) phy.setAnalogOutput(side, value) end ) end diff --git a/rtu/startup.lua b/rtu/startup.lua index d875ae6..577a13e 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -140,10 +140,19 @@ local function main() local rtu_redstone = config.Redstone local rtu_devices = config.Peripherals + -- get a string representation of a port interface + ---@param entry rtu_rs_definition + ---@return string + local function entry_iface_name(entry) + return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) + end + -- configure RTU gateway based on settings file definitions local function sys_config() -- redstone interfaces - local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table|nil, banks: IO_PORT[][] }[] + local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table|nil, banks: rtu_rs_definition[][] }[] + + local all_conns = {} -- go through redstone definitions list for entry_idx = 1, #rtu_redstone do @@ -152,7 +161,7 @@ local function main() local assignment local for_reactor = entry.unit local phy = entry.relay or 0 - local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) + 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 @@ -175,12 +184,12 @@ local function main() log.fatal(message) return false elseif not rs_rtus[entry.relay] then - log.debug(util.c("sys_config> allocated relay redstone RTU for interface ", entry.relay)) + log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay)) local relay = ppm.get_device(entry.relay) if not relay then - log.warning(util.c("sys_config> redstone relay ", entry.relay, " not connected")) + log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected")) elseif ppm.get_type(entry.relay) ~= "redstone_relay" then log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay")) end @@ -198,8 +207,9 @@ local function main() valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color)) end - local rs_rtu = rs_rtus[for_reactor].rtu - local conns = rs_rtus[phy].banks[for_reactor] + -- local rs_rtu = rs_rtus[phy].rtu + 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) @@ -216,10 +226,12 @@ local function main() println(message) log.warning(message) else - rs_rtu.link_di(entry.side, entry.color, entry.invert) + table.insert(bank, entry) + -- rs_rtu.link_di(entry.side, entry.color, entry.invert) end elseif mode == rsio.IO_MODE.DIGITAL_OUT then - rs_rtu.link_do(entry.side, entry.color, entry.invert) + table.insert(bank, entry) + -- rs_rtu.link_do(entry.side, entry.color, entry.invert) elseif mode == rsio.IO_MODE.ANALOG_IN then -- can't have duplicate inputs if util.table_contains(conns, entry.port) then @@ -227,25 +239,55 @@ local function main() println(message) log.warning(message) else - rs_rtu.link_ai(entry.side) + table.insert(bank, entry) + -- rs_rtu.link_ai(entry.side) end elseif mode == rsio.IO_MODE.ANALOG_OUT then - rs_rtu.link_ao(entry.side) + table.insert(bank, entry) + -- rs_rtu.link_ao(entry.side) else -- should be unreachable code, we already validated ports - log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true) + log.error("sys_config> failed to identify IO mode at block index #" .. entry_idx, true) println("sys_config> encountered a software error, check logs") return false end table.insert(conns, entry.port) - log.debug(util.c("sys_config> linked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment)) + log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, ") 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] + + -- link redstone to the RTU + for i = 1, #bank do + local conn = bank[i] + + 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) + 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), ")")) + end + end + local hw_state = util.trinary(def.phy, RTU_HW_STATE.OK, RTU_HW_STATE.OFFLINE) ---@class rtu_registry_entry @@ -256,7 +298,7 @@ local function main() index = false, ---@type integer|false reactor = nil, ---@type nil device = def.phy, ---@type table|nil - banks = def.banks, ---@type IO_PORT[][] + rs_conns = rtu_conns, ---@type IO_PORT[][]|nil is_multiblock = false, ---@type boolean formed = nil, ---@type boolean|nil hw_state = hw_state, ---@type RTU_HW_STATE diff --git a/rtu/threads.lua b/rtu/threads.lua index 7b872bf..e036a4e 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -133,7 +133,7 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit) elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then unit.rtu, faulted = envd_rtu.new(device) elseif unit.type == RTU_UNIT_TYPE.REDSTONE then - unit.rtu.change_phy(device) + unit.rtu.remount_phy(device) else unknown = true log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) From be462db50b03c49f06b5c4fd6024131c395203fc Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 29 Apr 2025 01:44:52 +0000 Subject: [PATCH 22/42] #604 new redstone initialization logic --- rtu/dev/redstone_rtu.lua | 18 ++++--- rtu/rtu.lua | 17 ++---- rtu/startup.lua | 112 +++++++++++++++++++++------------------ 3 files changed, 75 insertions(+), 72 deletions(-) diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 5772a10..185795e 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -44,8 +44,9 @@ function redstone_rtu.new(relay) ---@param side string ---@param color integer ---@param invert boolean|nil + ---@return integer count count of digital inputs function public.link_di(side, color, invert) - local f_read ---@type function + local f_read ---@type function if color then if invert then @@ -61,16 +62,17 @@ function redstone_rtu.new(relay) end end - unit.connect_di(f_read) + return unit.connect_di(f_read) end -- link digital output ---@param side string ---@param color integer ---@param invert boolean|nil + ---@return integer count count of digital outputs function public.link_do(side, color, invert) - local f_read ---@type function - local f_write ---@type function + local f_read ---@type function + local f_write ---@type function if color then if invert then @@ -123,19 +125,21 @@ function redstone_rtu.new(relay) end end - unit.connect_coil(f_read, f_write) + return unit.connect_coil(f_read, f_write) end -- link analog input ---@param side string + ---@return integer count count of analog inputs function public.link_ai(side) - unit.connect_input_reg(function () return phy.getAnalogInput(side) end) + return unit.connect_input_reg(function () return phy.getAnalogInput(side) end) end -- link analog output ---@param side string + ---@return integer count count of analog outputs function public.link_ao(side) - unit.connect_holding_reg( + return unit.connect_holding_reg( function () return phy.getAnalogOutput(side) end, function (value) phy.setAnalogOutput(side, value) end ) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index c7401fe..473b234 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -338,13 +338,7 @@ function rtu.comms(version, nic, conn_watchdog) local unit = units[i] if unit.type ~= nil then - local advert = { unit.type, unit.index, unit.reactor } - - if unit.type == RTU_UNIT_TYPE.REDSTONE then - insert(advert, unit.device) - end - - insert(advertisement, advert) + insert(advertisement, { unit.type, unit.index, unit.reactor or -1, unit.rs_conns }) end end @@ -479,12 +473,7 @@ function rtu.comms(version, nic, conn_watchdog) if unit.type == RTU_UNIT_TYPE.REDSTONE then -- immediately execute redstone RTU requests - if not unit.device then - reply = modbus.reply__srv_device_fail(packet) - return_code = false - else - return_code, reply = unit.modbus_io.handle_packet(packet) - end + return_code, reply = unit.modbus_io.handle_packet(packet) if not return_code then log.warning("requested MODBUS operation failed" .. unit_dbg_tag) @@ -502,7 +491,7 @@ function rtu.comms(version, nic, conn_watchdog) unit.pkt_queue.push_packet(packet) end else - log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag) + log.warning("requested MODBUS operation failed" .. unit_dbg_tag) end end else diff --git a/rtu/startup.lua b/rtu/startup.lua index 577a13e..1b3d4ad 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -149,9 +149,9 @@ local function main() -- configure RTU gateway based on settings file definitions local function sys_config() - -- redstone interfaces - local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table|nil, banks: rtu_rs_definition[][] }[] + --#region Redstone Interfaces + local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[] local all_conns = {} -- go through redstone definitions list @@ -161,6 +161,7 @@ local function main() 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 @@ -186,10 +187,12 @@ local function main() elseif not rs_rtus[entry.relay] then log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay)) - local relay = ppm.get_device(entry.relay) + local relay = ppm.get_device(entry.relay) if not relay then 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 log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay")) end @@ -208,8 +211,8 @@ local function main() end -- local rs_rtu = rs_rtus[phy].rtu - local bank = rs_rtus[phy].banks[for_reactor] - local conns = all_conns[for_reactor] + 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) @@ -222,54 +225,50 @@ local function main() 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) + 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) - -- rs_rtu.link_di(entry.side, entry.color, entry.invert) end - elseif mode == rsio.IO_MODE.DIGITAL_OUT then - table.insert(bank, entry) - -- rs_rtu.link_do(entry.side, entry.color, entry.invert) elseif mode == rsio.IO_MODE.ANALOG_IN then -- can't have duplicate inputs if util.table_contains(conns, entry.port) then - local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name) + 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) - -- rs_rtu.link_ai(entry.side) end - elseif mode == rsio.IO_MODE.ANALOG_OUT then + elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then table.insert(bank, entry) - -- rs_rtu.link_ao(entry.side) else -- should be unreachable code, we already validated ports - log.error("sys_config> failed to identify IO mode at block index #" .. entry_idx, true) + log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx) println("sys_config> encountered a software error, check logs") return false end table.insert(conns, entry.port) - log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment)) + log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment)) end end -- create unit entries for redstone RTUs for _, def in pairs(rs_rtus) do - local rtu_conns = { [0] = {}, {}, {}, {}, {}} + 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 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 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 @@ -280,44 +279,52 @@ local function main() 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), ")")) + 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 local hw_state = util.trinary(def.phy, RTU_HW_STATE.OK, RTU_HW_STATE.OFFLINE) - ---@class rtu_registry_entry + ---@type rtu_registry_entry local unit = { - uid = 0, ---@type integer - name = def.name, ---@type string - type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE - index = false, ---@type integer|false - reactor = nil, ---@type nil - device = def.phy, ---@type table|nil - rs_conns = rtu_conns, ---@type IO_PORT[][]|nil - is_multiblock = false, ---@type boolean - formed = nil, ---@type boolean|nil - hw_state = hw_state, ---@type RTU_HW_STATE - rtu = def.rtu, ---@type rtu_device|rtu_rs_device + uid = 0, + name = def.name, + type = RTU_UNIT_TYPE.REDSTONE, + index = false, + reactor = nil, + device = def.phy, + rs_conns = rtu_conns, + is_multiblock = false, + formed = nil, + hw_state = hw_state, + rtu = def.rtu, modbus_io = modbus.new(def.rtu, false), - pkt_queue = nil, ---@type mqueue|nil - thread = nil ---@type parallel_thread|nil + pkt_queue = nil, + thread = nil } table.insert(units, unit) - log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (redstone)")) + 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 - -- mounted peripherals + --#endregion + --#region Mounted Peripherals + for i = 1, #rtu_devices do local entry = rtu_devices[i] ---@type rtu_peri_definition local name = entry.name @@ -498,19 +505,20 @@ local function main() ---@class rtu_registry_entry local rtu_unit = { - uid = 0, ---@type integer - name = name, ---@type string - type = rtu_type, ---@type RTU_UNIT_TYPE - index = index or false, ---@type integer|false - reactor = for_reactor, ---@type integer - device = device, ---@type table peripheral reference - is_multiblock = is_multiblock, ---@type boolean - formed = formed, ---@type boolean|nil - hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE - rtu = rtu_iface, ---@type rtu_device|rtu_rs_device - modbus_io = modbus.new(rtu_iface, true), - pkt_queue = mqueue.new(), ---@type mqueue|nil - thread = nil ---@type parallel_thread|nil + uid = 0, ---@type integer RTU unit ID + name = name, ---@type string unit name + type = rtu_type, ---@type RTU_UNIT_TYPE unit type + index = index or false, ---@type integer|false device index + reactor = for_reactor, ---@type integer|nil unit/facility assignment + device = device, ---@type table peripheral reference + rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections + is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral + formed = formed, ---@type boolean|nil if this peripheral is currently formed + hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status + rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface + modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface + pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue + thread = nil ---@type parallel_thread|nil associated RTU thread } rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) @@ -544,6 +552,8 @@ local function main() databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) end + --#endregion + return true end @@ -612,7 +622,7 @@ local function main() -- run threads parallel.waitForAll(table.unpack(_threads)) else - println("configuration failed, exiting...") + println("system initialization failed, exiting...") end renderer.close_ui() From e6f5ab8ef439a5ee61ec79353fcbd9a0e500591c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 29 Apr 2025 02:38:42 +0000 Subject: [PATCH 23/42] #604 reworked supervisor redstone RTU interface --- scada-common/types.lua | 2 +- supervisor/facility.lua | 2 +- supervisor/session/rsctl.lua | 11 +- supervisor/session/rtu.lua | 43 +++-- supervisor/session/rtu/redstone.lua | 248 +++++++++++++++------------- supervisor/startup.lua | 2 +- supervisor/unit.lua | 2 +- 7 files changed, 176 insertions(+), 134 deletions(-) diff --git a/scada-common/types.lua b/scada-common/types.lua index 0d562a6..bc11516 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -125,7 +125,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end ---@field type RTU_UNIT_TYPE ---@field index integer|false ---@field reactor integer ----@field rsio IO_PORT[]|nil +---@field rs_conns IO_PORT[][]|nil -- create a new reactor database ---@nodiscard diff --git a/supervisor/facility.lua b/supervisor/facility.lua index f3cf18b..8d22f4b 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -157,7 +157,7 @@ function facility.new(config) self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd } -- init redstone RTU I/O controller - self.io_ctl = rsctl.new(self.redstone) + self.io_ctl = rsctl.new(self.redstone, 0) -- fill blank alarm/tone states for _ = 1, 12 do table.insert(self.test_alarm_states, false) end diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua index b270267..eb84cee 100644 --- a/supervisor/session/rsctl.lua +++ b/supervisor/session/rsctl.lua @@ -9,7 +9,8 @@ local rsctl = {} -- create a new redstone RTU I/O controller ---@nodiscard ---@param redstone_rtus redstone_session[] redstone RTU sessions -function rsctl.new(redstone_rtus) +---@param bank integer I/O bank (unit/facility assignment) to interface with +function rsctl.new(redstone_rtus, bank) ---@class rs_controller local public = {} @@ -18,7 +19,7 @@ function rsctl.new(redstone_rtus) ---@return boolean function public.is_connected(port) for i = 1, #redstone_rtus do - if redstone_rtus[i].get_db().io[port] ~= nil then return true end + if redstone_rtus[i].get_db().io[bank][port] ~= nil then return true end end return false @@ -29,7 +30,7 @@ function rsctl.new(redstone_rtus) ---@param value boolean function public.digital_write(port, value) for i = 1, #redstone_rtus do - local io = redstone_rtus[i].get_db().io[port] + local io = redstone_rtus[i].get_db().io[bank][port] if io ~= nil then io.write(value) end end end @@ -40,7 +41,7 @@ function rsctl.new(redstone_rtus) ---@return boolean|nil function public.digital_read(port) for i = 1, #redstone_rtus do - local io = redstone_rtus[i].get_db().io[port] + local io = redstone_rtus[i].get_db().io[bank][port] if io ~= nil then return io.read() --[[@as boolean|nil]] end end end @@ -52,7 +53,7 @@ function rsctl.new(redstone_rtus) ---@param max number maximum value for scaling 0 to 15 function public.analog_write(port, value, min, max) for i = 1, #redstone_rtus do - local io = redstone_rtus[i].get_db().io[port] + local io = redstone_rtus[i].get_db().io[bank][port] if io ~= nil then io.write(rsio.analog_write(value, min, max)) end end end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index a38559c..4f04a1e 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -93,7 +93,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad type = self.advert[i][1], index = self.advert[i][2], reactor = self.advert[i][3], - rsio = self.advert[i][4] + rs_conns = self.advert[i][4] } local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean @@ -105,13 +105,19 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad advert_validator.assert_type_int(unit_advert.reactor) if u_type == RTU_UNIT_TYPE.REDSTONE then - advert_validator.assert_type_table(unit_advert.rsio) + advert_validator.assert_type_table(unit_advert.rs_conns) end if advert_validator.valid() then if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end - advert_validator.assert_min(unit_advert.reactor, 0) - advert_validator.assert_max(unit_advert.reactor, #self.fac_units) + + if unit_advert.reactor == -1 then + advert_validator.assert_type_table(unit_advert.rs_conns) + else + advert_validator.assert_min(unit_advert.reactor, 0) + advert_validator.assert_max(unit_advert.reactor, #self.fac_units) + end + if not advert_validator.valid() then u_type = false end else u_type = false @@ -126,15 +132,32 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad -- validation fail log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure") else - if unit_advert.reactor > 0 then - local target_unit = self.fac_units[unit_advert.reactor] - - -- unit RTUs + if unit_advert.reactor == -1 then + -- redstone RTUs can be used in multiple different assignments if u_type == RTU_UNIT_TYPE.REDSTONE then -- redstone unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) - if type(unit) ~= "nil" then target_unit.add_redstone(unit) end - elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then + + -- link this to any subsystems this RTU provides connections for + if type(unit) ~= "nil" then + for assignment, _ in pairs(unit_advert.rs_conns) do + if assignment == 0 then + facility.add_redstone(unit) + elseif assignment > 0 and assignment < #self.fac_units then + self.fac_units[assignment].add_redstone(unit) + else + log.warning(util.c(log_tag, "_handle_advertisement(): unrecognized redstone RTU assignment ", assignment, " ", type_string)) + end + end + end + else + log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported multi-assignment RTU type ", type_string)) + end + elseif unit_advert.reactor > 0 then + local target_unit = self.fac_units[unit_advert.reactor] + + -- unit RTUs + if u_type == RTU_UNIT_TYPE.BOILER_VALVE then -- boiler unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q) if type(unit) ~= "nil" then target_unit.add_boiler(unit) end diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index ce9d6c4..610432d 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -39,6 +39,9 @@ local PERIODICS = { OUTPUT_SYNC = 200 } +-- create a new block of IO banks (facility, then each unit) +local function new_io_block() return { [0] = {}, {}, {}, {}, {} } end + ---@class dig_phy_entry ---@field phy IO_LVL actual value ---@field req IO_LVL commanded value @@ -74,27 +77,27 @@ function redstone.new(session_id, unit_id, advert, out_queue) next_ir_req = 0, next_hr_sync = 0 }, - ---@class rs_io_list - io_list = { - digital_in = {}, ---@type IO_PORT[] discrete inputs - digital_out = {}, ---@type IO_PORT[] coils - analog_in = {}, ---@type IO_PORT[] input registers - analog_out = {} ---@type IO_PORT[] holding registers + ---@class rs_io_map + io_map = { + digital_in = {}, ---@type { bank: integer, port: IO_PORT }[] discrete inputs + digital_out = {}, ---@type { bank: integer, port: IO_PORT }[] coils + analog_in = {}, ---@type { bank: integer, port: IO_PORT }[] input registers + analog_out = {} ---@type { bank: integer, port: IO_PORT }[] holding registers }, phy_trans = { coils = -1, hold_regs = -1 }, -- last set/read ports (reflecting the current state of the RTU) ---@class rs_io_states phy_io = { - digital_in = {}, ---@type dig_phy_entry[] discrete inputs - digital_out = {}, ---@type dig_phy_entry[] coils - analog_in = {}, ---@type ana_phy_entry[] input registers - analog_out = {} ---@type ana_phy_entry[] holding registers + digital_in = new_io_block(), ---@type dig_phy_entry[][] discrete inputs + digital_out = new_io_block(), ---@type dig_phy_entry[][] coils + analog_in = new_io_block(), ---@type ana_phy_entry[][] input registers + analog_out = new_io_block() ---@type ana_phy_entry[][] holding registers }, ---@class redstone_session_db db = { -- read/write functions for connected I/O - ---@type (rs_db_dig_io|rs_db_ana_io)[] - io = {} + ---@type (rs_db_dig_io|rs_db_ana_io)[][] + io = new_io_block() } } @@ -103,93 +106,91 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- INITIALIZE -- - -- create all ports as disconnected - for _ = 1, #IO_PORT do - table.insert(self.db, IO_LVL.DISCONNECT) - end - -- setup I/O - for i = 1, #advert.rsio do - local port = advert.rsio[i] + for bank = 0, 4 do + for i = 1, #advert.rs_conns[bank] do + local port = advert.rs_conns[bank][i] - if rsio.is_valid_port(port) then - local mode = rsio.get_io_mode(port) + if rsio.is_valid_port(port) then + local mode = rsio.get_io_mode(port) + local io_entry = { bank = bank, port = port } - if mode == IO_MODE.DIGITAL_IN then - self.has_di = true - table.insert(self.io_list.digital_in, port) + if mode == IO_MODE.DIGITAL_IN then + self.has_di = true + table.insert(self.io_map.digital_in, io_entry) - self.phy_io.digital_in[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } + self.phy_io.digital_in[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } - ---@class rs_db_dig_io - local io_f = { - ---@nodiscard - read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end, - write = function () end - } + ---@class rs_db_dig_io + local io_f = { + ---@nodiscard + read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[bank][port].phy) end, + write = function () end + } - self.db.io[port] = io_f - elseif mode == IO_MODE.DIGITAL_OUT then - self.has_do = true - table.insert(self.io_list.digital_out, port) + self.db.io[port] = io_f + elseif mode == IO_MODE.DIGITAL_OUT then + self.has_do = true + table.insert(self.io_map.digital_out, io_entry) - self.phy_io.digital_out[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } + self.phy_io.digital_out[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } - ---@class rs_db_dig_io - local io_f = { - ---@nodiscard - read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end, - ---@param active boolean - write = function (active) - local level = rsio.digital_write_active(port, active) - if level ~= nil then self.phy_io.digital_out[port].req = level end - end - } - - self.db.io[port] = io_f - elseif mode == IO_MODE.ANALOG_IN then - self.has_ai = true - table.insert(self.io_list.analog_in, port) - - self.phy_io.analog_in[port] = { phy = 0, req = 0 } - - ---@class rs_db_ana_io - local io_f = { - ---@nodiscard - ---@return integer - read = function () return self.phy_io.analog_in[port].phy end, - write = function () end - } - - self.db.io[port] = io_f - elseif mode == IO_MODE.ANALOG_OUT then - self.has_ao = true - table.insert(self.io_list.analog_out, port) - - self.phy_io.analog_out[port] = { phy = 0, req = 0 } - - ---@class rs_db_ana_io - local io_f = { - ---@nodiscard - ---@return integer - read = function () return self.phy_io.analog_out[port].phy end, - ---@param value integer - write = function (value) - if value >= 0 and value <= 15 then - self.phy_io.analog_out[port].req = value + ---@class rs_db_dig_io + local io_f = { + ---@nodiscard + read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[bank][port].phy) end, + ---@param active boolean + write = function (active) + local level = rsio.digital_write_active(port, active) + if level ~= nil then self.phy_io.digital_out[bank][port].req = level end end - end - } + } - self.db.io[port] = io_f + self.db.io[port] = io_f + elseif mode == IO_MODE.ANALOG_IN then + self.has_ai = true + table.insert(self.io_map.analog_in, io_entry) + + self.phy_io.analog_in[bank][port] = { phy = 0, req = 0 } + + ---@class rs_db_ana_io + local io_f = { + ---@nodiscard + ---@return integer + read = function () return self.phy_io.analog_in[bank][port].phy end, + write = function () end + } + + self.db.io[port] = io_f + elseif mode == IO_MODE.ANALOG_OUT then + self.has_ao = true + table.insert(self.io_map.analog_out, io_entry) + + self.phy_io.analog_out[bank][port] = { phy = 0, req = 0 } + + ---@class rs_db_ana_io + local io_f = { + ---@nodiscard + ---@return integer + read = function () return self.phy_io.analog_out[bank][port].phy end, + ---@param value integer + write = function (value) + if value >= 0 and value <= 15 then + self.phy_io.analog_out[bank][port].req = value + end + end + } + + self.db.io[port] = io_f + else + -- should be unreachable code, we already validated ports + log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true) + return nil + end else - -- should be unreachable code, we already validated ports - log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true) + log.error(util.c(log_tag, "invalid advertisement port (", bank, ":", port, ")"), true) return nil end - else - log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true) - return nil end end @@ -197,12 +198,12 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- query discrete inputs local function _request_discrete_inputs() - self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in }) + self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_map.digital_in }) end -- query input registers local function _request_input_registers() - self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) + self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_map.analog_in }) end -- write all coil outputs @@ -210,9 +211,9 @@ function redstone.new(session_id, unit_id, advert, out_queue) local params = { 1 } local outputs = self.phy_io.digital_out - for i = 1, #self.io_list.digital_out do - local port = self.io_list.digital_out[i] - table.insert(params, outputs[port].req) + for i = 1, #self.io_map.digital_out do + local entry = self.io_map.digital_out[i] + table.insert(params, outputs[entry.bank][entry.port].req) end self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params) @@ -220,7 +221,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- read all coil outputs local function _read_coils() - self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_list.digital_out }) + self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_map.digital_out }) end -- write all holding register outputs @@ -228,9 +229,9 @@ function redstone.new(session_id, unit_id, advert, out_queue) local params = { 1 } local outputs = self.phy_io.analog_out - for i = 1, #self.io_list.analog_out do - local port = self.io_list.analog_out[i] - table.insert(params, outputs[port].req) + for i = 1, #self.io_map.analog_out do + local entry = self.io_map.analog_out[i] + table.insert(params, outputs[entry.bank][entry.port].req) end self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params) @@ -238,7 +239,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- read all holding register outputs local function _read_holding_registers() - self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_list.analog_out }) + self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_map.analog_out }) end -- PUBLIC FUNCTIONS -- @@ -259,24 +260,24 @@ function redstone.new(session_id, unit_id, advert, out_queue) end elseif txn_type == TXN_TYPES.DI_READ then -- discrete input read response - if m_pkt.length == #self.io_list.digital_in then + if m_pkt.length == #self.io_map.digital_in then for i = 1, m_pkt.length do - local port = self.io_list.digital_in[i] + local entry = self.io_map.digital_in[i] local value = m_pkt.data[i] - self.phy_io.digital_in[port].phy = value + self.phy_io.digital_in[entry.bank][entry.port].phy = value end else log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") end elseif txn_type == TXN_TYPES.INPUT_REG_READ then -- input register read response - if m_pkt.length == #self.io_list.analog_in then + if m_pkt.length == #self.io_map.analog_in then for i = 1, m_pkt.length do - local port = self.io_list.analog_in[i] + local entry = self.io_map.analog_in[i] local value = m_pkt.data[i] - self.phy_io.analog_in[port].phy = value + self.phy_io.analog_in[entry.bank][entry.port].phy = value end else log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") @@ -288,15 +289,14 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- update phy I/O table -- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical) -- given these are redstone outputs, if one worked they all should have, so no additional verification will be done - if m_pkt.length == #self.io_list.digital_out then + if m_pkt.length == #self.io_map.digital_out then for i = 1, m_pkt.length do - local port = self.io_list.digital_out[i] + local entry = self.io_map.digital_out[i] + local state = self.phy_io.digital_out[entry.bank][entry.port] local value = m_pkt.data[i] - self.phy_io.digital_out[port].phy = value - if self.phy_io.digital_out[port].req == IO_LVL.FLOATING then - self.phy_io.digital_out[port].req = value - end + state.phy = value + if state.req == IO_LVL.FLOATING then state.req = value end end self.phy_trans.coils = TXN_READY @@ -310,12 +310,12 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- update phy I/O table -- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical) -- given these are redstone outputs, if one worked they all should have, so no additional verification will be done - if m_pkt.length == #self.io_list.analog_out then + if m_pkt.length == #self.io_map.analog_out then for i = 1, m_pkt.length do - local port = self.io_list.analog_out[i] + local entry = self.io_map.analog_out[i] local value = m_pkt.data[i] - self.phy_io.analog_out[port].phy = value + self.phy_io.analog_out[entry.bank][entry.port].phy = value end else log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") @@ -343,8 +343,17 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- sync digital outputs if self.has_do then if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then - for _, entry in pairs(self.phy_io.digital_out) do - if entry.phy ~= entry.req then + for bank = 0, 4 do + local changed = false + + for _, entry in pairs(self.phy_io.digital_out[bank]) do + if entry.phy ~= entry.req then + changed = true + break + end + end + + if changed then _write_coils() break end @@ -365,8 +374,17 @@ function redstone.new(session_id, unit_id, advert, out_queue) -- sync analog outputs if self.has_ao then if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then - for _, entry in pairs(self.phy_io.analog_out) do - if entry.phy ~= entry.req then + for bank = 0, 4 do + local changed = false + + for _, entry in pairs(self.phy_io.analog_out[bank]) do + if entry.phy ~= entry.req then + changed = true + break + end + end + + if changed then _write_holding_registers() break end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d1ee16d..4a88018 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -23,7 +23,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.6.8" +local SUPERVISOR_VERSION = "v1.7.0" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index b448a95..9e22824 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -258,7 +258,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd } -- init redstone RTU I/O controller - self.io_ctl = rsctl.new(self.redstone) + self.io_ctl = rsctl.new(self.redstone, reactor_id) -- init boiler table fields for _ = 1, num_boilers do From eafd39fa3574fea021af7ee8b44ab5718f0842ac Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 29 Apr 2025 19:41:52 +0000 Subject: [PATCH 24/42] #572 added facility radiation alarm --- scada-common/types.lua | 6 +- scada-common/util.lua | 2 +- supervisor/alarm_ctl.lua | 134 +++++++++++++++++++++++++++++++++ supervisor/facility.lua | 31 +++++++- supervisor/facility_update.lua | 17 ++++- supervisor/startup.lua | 2 +- supervisor/unit.lua | 36 +++------ supervisor/unitlogic.lua | 116 ++++------------------------ 8 files changed, 204 insertions(+), 140 deletions(-) create mode 100644 supervisor/alarm_ctl.lua diff --git a/scada-common/types.lua b/scada-common/types.lua index 0d562a6..06f7c8d 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -465,7 +465,8 @@ types.ALARM = { ReactorHighWaste = 9, RPSTransient = 10, RCSTransient = 11, - TurbineTrip = 12 + TurbineTrip = 12, + FacilityRadiation = 13 } types.ALARM_NAMES = { @@ -480,7 +481,8 @@ types.ALARM_NAMES = { "ReactorHighWaste", "RPSTransient", "RCSTransient", - "TurbineTrip" + "TurbineTrip", + "FacilityRadiation" } ---@enum ALARM_PRIORITY diff --git a/scada-common/util.lua b/scada-common/util.lua index faa614c..2a2f81a 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -24,7 +24,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.5.1" +util.version = "1.5.2" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 diff --git a/supervisor/alarm_ctl.lua b/supervisor/alarm_ctl.lua new file mode 100644 index 0000000..9ebf410 --- /dev/null +++ b/supervisor/alarm_ctl.lua @@ -0,0 +1,134 @@ +local log = require("scada-common.log") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local ALARM_STATE = types.ALARM_STATE + +---@class alarm_def +---@field state ALARM_INT_STATE internal alarm state +---@field trip_time integer time (ms) when first tripped +---@field hold_time integer time (s) to hold before tripping +---@field id ALARM alarm ID +---@field tier integer alarm urgency tier (0 = highest) + +local AISTATE_NAMES = { + "INACTIVE", + "TRIPPING", + "TRIPPED", + "ACKED", + "RING_BACK", + "RING_BACK_TRIPPING" +} + +---@enum ALARM_INT_STATE +local AISTATE = { + INACTIVE = 1, + TRIPPING = 2, + TRIPPED = 3, + ACKED = 4, + RING_BACK = 5, + RING_BACK_TRIPPING = 6 +} + +local alarm_ctl = {} + +alarm_ctl.AISTATE = AISTATE +alarm_ctl.AISTATE_NAMES = AISTATE_NAMES + +-- update an alarm state given conditions +---@param caller_tag string tag to use in log messages +---@param alarm_states { [ALARM]: ALARM_STATE } unit instance +---@param tripped boolean if the alarm condition is sti ll active +---@param alarm alarm_def alarm table +---@param no_ring_back boolean? true to skip the ring back state, returning to inactive instead +---@return boolean new_trip if the alarm just changed to being tripped +function alarm_ctl.update_alarm_state(caller_tag, alarm_states, tripped, alarm, no_ring_back) + local int_state = alarm.state + local ext_state = alarm_states[alarm.id] + + -- alarm inactive + if int_state == AISTATE.INACTIVE then + if tripped then + alarm.trip_time = util.time_ms() + if alarm.hold_time > 0 then + alarm.state = AISTATE.TRIPPING + alarm_states[alarm.id] = ALARM_STATE.INACTIVE + else + alarm.state = AISTATE.TRIPPED + alarm_states[alarm.id] = ALARM_STATE.TRIPPED + log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ", + types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) + end + else + alarm.trip_time = util.time_ms() + alarm_states[alarm.id] = ALARM_STATE.INACTIVE + end + -- alarm condition met, but not yet for required hold time + elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then + if tripped then + local elapsed = util.time_ms() - alarm.trip_time + if elapsed > (alarm.hold_time * 1000) then + alarm.state = AISTATE.TRIPPED + alarm_states[alarm.id] = ALARM_STATE.TRIPPED + log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ", + types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) + end + elseif int_state == AISTATE.RING_BACK_TRIPPING then + alarm.trip_time = 0 + alarm.state = AISTATE.RING_BACK + alarm_states[alarm.id] = ALARM_STATE.RING_BACK + else + alarm.trip_time = 0 + alarm.state = AISTATE.INACTIVE + alarm_states[alarm.id] = ALARM_STATE.INACTIVE + end + -- alarm tripped and alarming + elseif int_state == AISTATE.TRIPPED then + if tripped then + if ext_state == ALARM_STATE.ACKED then + -- was acked by coordinator + alarm.state = AISTATE.ACKED + end + else + alarm.state = AISTATE.RING_BACK + alarm_states[alarm.id] = ALARM_STATE.RING_BACK + end + -- alarm acknowledged but still tripped + elseif int_state == AISTATE.ACKED then + if not tripped then + if no_ring_back then + alarm.state = AISTATE.INACTIVE + alarm_states[alarm.id] = ALARM_STATE.INACTIVE + else + alarm.state = AISTATE.RING_BACK + alarm_states[alarm.id] = ALARM_STATE.RING_BACK + end + end + -- alarm no longer tripped, operator must reset to clear + elseif int_state == AISTATE.RING_BACK then + if tripped then + alarm.trip_time = util.time_ms() + if alarm.hold_time > 0 then + alarm.state = AISTATE.RING_BACK_TRIPPING + else + alarm.state = AISTATE.TRIPPED + alarm_states[alarm.id] = ALARM_STATE.TRIPPED + end + elseif ext_state == ALARM_STATE.INACTIVE then + -- was reset by coordinator + alarm.state = AISTATE.INACTIVE + alarm.trip_time = 0 + end + else + log.error(util.c(caller_tag, " invalid alarm state for alarm ", alarm.id), true) + end + + -- check for state change + if alarm.state ~= int_state then + local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state]) + log.debug(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str)) + return alarm.state == AISTATE.TRIPPED + else return false end +end + +return alarm_ctl diff --git a/supervisor/facility.lua b/supervisor/facility.lua index f3cf18b..ca2369a 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -2,13 +2,19 @@ local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") +local alarm_ctl = require("supervisor.alarm_ctl") local unit = require("supervisor.unit") local fac_update = require("supervisor.facility_update") local rsctl = require("supervisor.session.rsctl") local svsessions = require("supervisor.session.svsessions") +local AISTATE = alarm_ctl.AISTATE + +local ALARM = types.ALARM +local ALARM_STATE = types.ALARM_STATE local AUTO_GROUP = types.AUTO_GROUP +local PRIO = types.ALARM_PRIORITY local PROCESS = types.PROCESS local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE @@ -138,7 +144,17 @@ function facility.new(config) imtx_last_charge = 0, imtx_last_charge_t = 0, -- track faulted induction matrix update times to reject - imtx_faulted_times = { 0, 0, 0 } + imtx_faulted_times = { 0, 0, 0 }, + -- facility alarms + ---@type { [string]: alarm_def } + alarms = { + -- radiation monitor alarm for the facility + FacilityRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.FacilityRadiation, tier = PRIO.CRITICAL }, + }, + ---@type { [ALARM]: ALARM_STATE } + alarm_states = { + [ALARM.FacilityRadiation] = ALARM_STATE.INACTIVE + } } --#region SETUP @@ -335,6 +351,9 @@ function facility.new(config) -- unit tasks f_update.unit_mgmt() + -- update alarm states right before updating the audio + f_update.update_alarms() + -- update alarm tones f_update.alarm_audio() end @@ -404,10 +423,14 @@ function facility.new(config) end end - -- ack all alarms on all reactor units + -- ack all alarms on all reactor units and the facility function public.ack_all() - for i = 1, #self.units do - self.units[i].ack_all() + -- unit alarms + for i = 1, #self.units do self.units[i].ack_all() end + + -- facility alarms + for id, state in pairs(self.alarm_states) do + if state == ALARM_STATE.TRIPPED then self.alarm_states[id] = ALARM_STATE.ACKED end end end diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index a127f81..62dd5fb 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -5,6 +5,8 @@ local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") +local alarm_ctl = require("supervisor.alarm_ctl") + local plc = require("supervisor.session.plc") local svsessions = require("supervisor.session.svsessions") @@ -714,11 +716,17 @@ function update.post_auto() self.mode = next_mode end +-- update facility alarm states +function update.update_alarms() + -- Facility Radiation + alarm_ctl.update_alarm_state("FAC", self.alarm_states, self.ascram_status.radiation, self.alarms.FacilityRadiation, true) +end + -- update alarm audio control function update.alarm_audio() local allow_test = self.allow_testing and self.test_tone_set - local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } + local alarms = { false, false, false, false, false, false, false, false, false, false, false, false, false } -- reset tone states before re-evaluting for i = 1, #self.tone_states do self.tone_states[i] = false end @@ -734,8 +742,11 @@ function update.alarm_audio() end end + -- record facility alarms + alarms[ALARM.FacilityRadiation] = self.alarm_states[ALARM.FacilityRadiation] == ALARM_STATE.TRIPPED + + -- clear testing alarms if we aren't using them if not self.test_tone_reset then - -- clear testing alarms if we aren't using them for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end end end @@ -774,7 +785,7 @@ function update.alarm_audio() end -- radiation is a big concern, always play this CRITICAL level alarm if active - if alarms[ALARM.ContainmentRadiation] then + if alarms[ALARM.ContainmentRadiation] or alarms[ALARM.FacilityRadiation] then self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true -- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled -- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d1ee16d..fa0e1a6 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -23,7 +23,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.6.8" +local SUPERVISOR_VERSION = "v1.6.9" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index b448a95..e2bb4f9 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -3,20 +3,23 @@ local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") +local alarmctl = require("supervisor.alarm_ctl") local logic = require("supervisor.unitlogic") local plc = require("supervisor.session.plc") local rsctl = require("supervisor.session.rsctl") local svsessions = require("supervisor.session.svsessions") -local WASTE_MODE = types.WASTE_MODE -local WASTE = types.WASTE_PRODUCT +local AISTATE = alarmctl.AISTATE + local ALARM = types.ALARM -local PRIO = types.ALARM_PRIORITY local ALARM_STATE = types.ALARM_STATE -local TRI_FAIL = types.TRI_FAIL +local PRIO = types.ALARM_PRIORITY local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE +local TRI_FAIL = types.TRI_FAIL +local WASTE_MODE = types.WASTE_MODE +local WASTE = types.WASTE_PRODUCT local PLC_S_CMDS = plc.PLC_S_CMDS @@ -37,23 +40,6 @@ local DT_KEYS = { TurbinePower = "TPR" } ----@enum ALARM_INT_STATE -local AISTATE = { - INACTIVE = 1, - TRIPPING = 2, - TRIPPED = 3, - ACKED = 4, - RING_BACK = 5, - RING_BACK_TRIPPING = 6 -} - ----@class alarm_def ----@field state ALARM_INT_STATE internal alarm state ----@field trip_time integer time (ms) when first tripped ----@field hold_time integer time (s) to hold before tripping ----@field id ALARM alarm ID ----@field tier integer alarm urgency tier (0 = highest) - -- burn rate to idle at local IDLE_RATE = 0.01 @@ -81,7 +67,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) num_boilers = num_boilers, num_turbines = num_turbines, aux_coolant = aux_coolant, - types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, + types = { DT_KEYS = DT_KEYS }, -- rtus rtu_list = {}, ---@type unit_session[][] redstone = {}, ---@type redstone_session[] @@ -775,10 +761,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) -- acknowledge all alarms (if possible) function public.ack_all() - for i = 1, #self.db.alarm_states do - if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then - self.db.alarm_states[i] = ALARM_STATE.ACKED - end + for id, state in pairs(self.db.alarm_states) do + if state == ALARM_STATE.TRIPPED then self.db.alarm_states[id] = ALARM_STATE.ACKED end end end diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 6bd7470..3444462 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -1,12 +1,16 @@ -local const = require("scada-common.constants") -local log = require("scada-common.log") -local rsio = require("scada-common.rsio") -local types = require("scada-common.types") -local util = require("scada-common.util") +local const = require("scada-common.constants") +local log = require("scada-common.log") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") -local plc = require("supervisor.session.plc") +local alarm_ctl = require("supervisor.alarm_ctl") -local qtypes = require("supervisor.session.rtu.qtypes") +local plc = require("supervisor.session.plc") + +local qtypes = require("supervisor.session.rtu.qtypes") + +local AISTATE = alarm_ctl.AISTATE local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE local TRI_FAIL = types.TRI_FAIL @@ -22,15 +26,6 @@ local IO = rsio.IO local PLC_S_CMDS = plc.PLC_S_CMDS -local AISTATE_NAMES = { - "INACTIVE", - "TRIPPING", - "TRIPPED", - "ACKED", - "RING_BACK", - "RING_BACK_TRIPPING" -} - local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS @@ -431,88 +426,7 @@ end ---@param alarm alarm_def alarm table ---@return boolean new_trip if the alarm just changed to being tripped local function _update_alarm_state(self, tripped, alarm) - local AISTATE = self.types.AISTATE - local int_state = alarm.state - local ext_state = self.db.alarm_states[alarm.id] - - -- alarm inactive - if int_state == AISTATE.INACTIVE then - if tripped then - alarm.trip_time = util.time_ms() - if alarm.hold_time > 0 then - alarm.state = AISTATE.TRIPPING - self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE - else - alarm.state = AISTATE.TRIPPED - self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED - log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ", - types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) - end - else - alarm.trip_time = util.time_ms() - self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE - end - -- alarm condition met, but not yet for required hold time - elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then - if tripped then - local elapsed = util.time_ms() - alarm.trip_time - if elapsed > (alarm.hold_time * 1000) then - alarm.state = AISTATE.TRIPPED - self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED - log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ", - types.ALARM_PRIORITY_NAMES[alarm.tier],"]")) - end - elseif int_state == AISTATE.RING_BACK_TRIPPING then - alarm.trip_time = 0 - alarm.state = AISTATE.RING_BACK - self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK - else - alarm.trip_time = 0 - alarm.state = AISTATE.INACTIVE - self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE - end - -- alarm tripped and alarming - elseif int_state == AISTATE.TRIPPED then - if tripped then - if ext_state == ALARM_STATE.ACKED then - -- was acked by coordinator - alarm.state = AISTATE.ACKED - end - else - alarm.state = AISTATE.RING_BACK - self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK - end - -- alarm acknowledged but still tripped - elseif int_state == AISTATE.ACKED then - if not tripped then - alarm.state = AISTATE.RING_BACK - self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK - end - -- alarm no longer tripped, operator must reset to clear - elseif int_state == AISTATE.RING_BACK then - if tripped then - alarm.trip_time = util.time_ms() - if alarm.hold_time > 0 then - alarm.state = AISTATE.RING_BACK_TRIPPING - else - alarm.state = AISTATE.TRIPPED - self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED - end - elseif ext_state == ALARM_STATE.INACTIVE then - -- was reset by coordinator - alarm.state = AISTATE.INACTIVE - alarm.trip_time = 0 - end - else - log.error(util.c("invalid alarm state for unit ", self.r_id, " alarm ", alarm.id), true) - end - - -- check for state change - if alarm.state ~= int_state then - local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state]) - log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str)) - return alarm.state == AISTATE.TRIPPED - else return false end + return alarm_ctl.update_alarm_state("UNIT " .. self.r_id, self.db.alarm_states, tripped, alarm) end -- evaluate alarm conditions @@ -632,8 +546,6 @@ end ---@param public reactor_unit reactor unit public functions ---@param self _unit_self unit instance function logic.update_auto_safety(public, self) - local AISTATE = self.types.AISTATE - if self.auto_engaged then local alarmed = false @@ -662,7 +574,6 @@ end -- update the two unit status text messages ---@param self _unit_self unit instance function logic.update_status_text(self) - local AISTATE = self.types.AISTATE local annunc = self.db.annunciator -- check if an alarm is active (tripped or ack'd) @@ -826,7 +737,6 @@ end -- handle unit redstone I/O ---@param self _unit_self unit instance function logic.handle_redstone(self) - local AISTATE = self.types.AISTATE local annunc = self.db.annunciator local cache = self.plc_cache local rps = cache.rps_status @@ -906,7 +816,7 @@ function logic.handle_redstone(self) if enable_emer_cool and not self.em_cool_opened then log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<")) log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]")) - log.debug(util.c("| ReactorOverTemp[", AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]")) + log.debug(util.c("| ReactorOverTemp[", alarm_ctl.AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]")) for i = 1, #annunc.WaterLevelLow do log.debug(util.c("| WaterLevelLow(", i, ")[", annunc.WaterLevelLow[i], "]")) From b8c30ba8a4a491ecfdbc5c865e5fca349320f236 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 29 Apr 2025 19:47:29 +0000 Subject: [PATCH 25/42] cleanup --- supervisor/alarm_ctl.lua | 2 +- supervisor/unit.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/supervisor/alarm_ctl.lua b/supervisor/alarm_ctl.lua index 9ebf410..d238fd9 100644 --- a/supervisor/alarm_ctl.lua +++ b/supervisor/alarm_ctl.lua @@ -35,7 +35,7 @@ local alarm_ctl = {} alarm_ctl.AISTATE = AISTATE alarm_ctl.AISTATE_NAMES = AISTATE_NAMES --- update an alarm state given conditions +-- update an alarm state based on its current status and if it is tripped ---@param caller_tag string tag to use in log messages ---@param alarm_states { [ALARM]: ALARM_STATE } unit instance ---@param tripped boolean if the alarm condition is sti ll active diff --git a/supervisor/unit.lua b/supervisor/unit.lua index e2bb4f9..507ebcd 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -3,14 +3,14 @@ local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") -local alarmctl = require("supervisor.alarm_ctl") +local alarm_ctl = require("supervisor.alarm_ctl") local logic = require("supervisor.unitlogic") local plc = require("supervisor.session.plc") local rsctl = require("supervisor.session.rsctl") local svsessions = require("supervisor.session.svsessions") -local AISTATE = alarmctl.AISTATE +local AISTATE = alarm_ctl.AISTATE local ALARM = types.ALARM local ALARM_STATE = types.ALARM_STATE From 0df1e48780b539820c4b6c36970bf619b3bef245 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 29 Apr 2025 20:11:12 +0000 Subject: [PATCH 26/42] reorganized unit logic inclusion to work like facility update --- supervisor/unit.lua | 15 +++--- supervisor/{unitlogic.lua => unit_logic.lua} | 56 ++++++++++---------- 2 files changed, 37 insertions(+), 34 deletions(-) rename supervisor/{unitlogic.lua => unit_logic.lua} (95%) diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 507ebcd..7f5e50b 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -4,7 +4,7 @@ local types = require("scada-common.types") local util = require("scada-common.util") local alarm_ctl = require("supervisor.alarm_ctl") -local logic = require("supervisor.unitlogic") +local unit_logic = require("supervisor.unit_logic") local plc = require("supervisor.session.plc") local rsctl = require("supervisor.session.rsctl") @@ -240,6 +240,9 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) } } + -- provide self to unit logic functions + local logic = unit_logic(self) + -- list for RTU session management self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd } @@ -583,20 +586,20 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) _dt__compute_all() -- update annunciator logic - logic.update_annunciator(self) + logic.update_annunciator() -- update alarm status - logic.update_alarms(self) + logic.update_alarms() -- if in auto mode, SCRAM on certain alarms - logic.update_auto_safety(public, self) + logic.update_auto_safety(public) -- update status text - logic.update_status_text(self) + logic.update_status_text() -- handle redstone I/O if #self.redstone > 0 then - logic.handle_redstone(self) + logic.handle_redstone() elseif not self.plc_cache.rps_trip then self.em_cool_opened = false end diff --git a/supervisor/unitlogic.lua b/supervisor/unit_logic.lua similarity index 95% rename from supervisor/unitlogic.lua rename to supervisor/unit_logic.lua index 3444462..fd6a67f 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unit_logic.lua @@ -26,18 +26,18 @@ local IO = rsio.IO local PLC_S_CMDS = plc.PLC_S_CMDS +local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS +local ALARM_LIMS = const.ALARM_LIMITS local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS +local RS_THRESH = const.RS_THRESHOLDS -local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS -local ALARM_LIMS = const.ALARM_LIMITS -local RS_THRESH = const.RS_THRESHOLDS +local self = nil ---@type _unit_self ---@class unit_logic_extension local logic = {} -- update the annunciator ----@param self _unit_self -function logic.update_annunciator(self) +function logic.update_annunciator() local DT_KEYS = self.types.DT_KEYS local _get_dt = self._get_dt @@ -421,23 +421,21 @@ function logic.update_annunciator(self) end -- update an alarm state given conditions ----@param self _unit_self unit instance ---@param tripped boolean if the alarm condition is still active ---@param alarm alarm_def alarm table ---@return boolean new_trip if the alarm just changed to being tripped -local function _update_alarm_state(self, tripped, alarm) +local function _update_alarm_state(tripped, alarm) return alarm_ctl.update_alarm_state("UNIT " .. self.r_id, self.db.alarm_states, tripped, alarm) end -- evaluate alarm conditions ----@param self _unit_self unit instance -function logic.update_alarms(self) +function logic.update_alarms() local annunc = self.db.annunciator local plc_cache = self.plc_cache -- Containment Breach -- lost plc with critical damage (rip plc, you will be missed) - _update_alarm_state(self, (not plc_cache.ok) and (plc_cache.damage > 99), self.alarms.ContainmentBreach) + _update_alarm_state((not plc_cache.ok) and (plc_cache.damage > 99), self.alarms.ContainmentBreach) -- Containment Radiation local rad_alarm = false @@ -446,38 +444,38 @@ function logic.update_alarms(self) rad_alarm = self.last_radiation >= ALARM_LIMS.HIGH_RADIATION break end - _update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation) + _update_alarm_state(rad_alarm, self.alarms.ContainmentRadiation) -- Reactor Lost - _update_alarm_state(self, self.had_reactor and self.plc_i == nil, self.alarms.ReactorLost) + _update_alarm_state(self.had_reactor and self.plc_i == nil, self.alarms.ReactorLost) -- Critical Damage - _update_alarm_state(self, plc_cache.damage >= 100, self.alarms.CriticalDamage) + _update_alarm_state(plc_cache.damage >= 100, self.alarms.CriticalDamage) -- Reactor Damage local rps_dmg_90 = plc_cache.rps_status.high_dmg and not self.last_rps_trips.high_dmg - if _update_alarm_state(self, (plc_cache.damage > 0) or rps_dmg_90, self.alarms.ReactorDamage) then + if _update_alarm_state((plc_cache.damage > 0) or rps_dmg_90, self.alarms.ReactorDamage) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorDamage.id]," <<")) log.debug(util.c("| plc_cache.damage[", plc_cache.damage, "] rps_dmg_90[", rps_dmg_90, "]")) end -- Over-Temperature local rps_high_temp = plc_cache.rps_status.high_temp and not self.last_rps_trips.high_temp - if _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) then + if _update_alarm_state((plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorOverTemp.id]," <<")) log.debug(util.c("| plc_cache.temp[", plc_cache.temp, "] rps_high_temp[", rps_high_temp, "]")) end -- High Temperature local high_temp = math.min(math.max(self.plc_cache.high_temp_lim, 1100), 1199.995) - _update_alarm_state(self, plc_cache.temp >= high_temp, self.alarms.ReactorHighTemp) + _update_alarm_state(plc_cache.temp >= high_temp, self.alarms.ReactorHighTemp) -- Waste Leak - _update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) + _update_alarm_state(plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) -- High Waste local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste - if _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) then + if _update_alarm_state((plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorHighWaste.id]," <<")) log.debug(util.c("| plc_cache.waste[", plc_cache.waste, "] rps_high_waste[", rps_high_waste, "]")) end @@ -492,7 +490,7 @@ function logic.update_alarms(self) end end - _update_alarm_state(self, rps_alarm, self.alarms.RPSTransient) + _update_alarm_state(rps_alarm, self.alarms.RPSTransient) -- RCS Transient local any_low = annunc.CoolantLevelLow @@ -525,7 +523,7 @@ function logic.update_alarms(self) end end - if _update_alarm_state(self, rcs_trans, self.alarms.RCSTransient) then + if _update_alarm_state(rcs_trans, self.alarms.RCSTransient) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.RCSTransient.id]," <<")) log.debug(util.c("| any_low[", any_low, "] any_over[", any_over, "] gen_trip[", gen_trip, "]")) log.debug(util.c("| RCPTrip[", annunc.RCPTrip, "] MaxWaterReturnFeed[", annunc.MaxWaterReturnFeed, "]")) @@ -536,7 +534,7 @@ function logic.update_alarms(self) -- Turbine Trip local any_trip = false for i = 1, #annunc.TurbineTrip do any_trip = any_trip or annunc.TurbineTrip[i] end - _update_alarm_state(self, any_trip, self.alarms.TurbineTrip) + _update_alarm_state(any_trip, self.alarms.TurbineTrip) -- update last trips table for key, val in pairs(plc_cache.rps_status) do self.last_rps_trips[key] = val end @@ -544,8 +542,7 @@ end -- update the internal automatic safety control performed while in auto control mode ---@param public reactor_unit reactor unit public functions ----@param self _unit_self unit instance -function logic.update_auto_safety(public, self) +function logic.update_auto_safety(public) if self.auto_engaged then local alarmed = false @@ -572,8 +569,7 @@ function logic.update_auto_safety(public, self) end -- update the two unit status text messages ----@param self _unit_self unit instance -function logic.update_status_text(self) +function logic.update_status_text() local annunc = self.db.annunciator -- check if an alarm is active (tripped or ack'd) @@ -735,8 +731,7 @@ function logic.update_status_text(self) end -- handle unit redstone I/O ----@param self _unit_self unit instance -function logic.handle_redstone(self) +function logic.handle_redstone() local annunc = self.db.annunciator local cache = self.plc_cache local rps = cache.rps_status @@ -893,4 +888,9 @@ function logic.handle_redstone(self) end end -return logic +-- link the self instance and return the logic interface +---@param unit_self _unit_self +return function (unit_self) + self = unit_self + return logic +end From 9393b1830d05637e01f66c0fd7ed49bf5da62dfc Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 30 Apr 2025 10:17:09 -0400 Subject: [PATCH 27/42] restored use of self for unit logic function calls --- supervisor/unit.lua | 13 ++++------ supervisor/unit_logic.lua | 51 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 7f5e50b..6129d04 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -240,9 +240,6 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) } } - -- provide self to unit logic functions - local logic = unit_logic(self) - -- list for RTU session management self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd } @@ -586,20 +583,20 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant) _dt__compute_all() -- update annunciator logic - logic.update_annunciator() + unit_logic.update_annunciator(self) -- update alarm status - logic.update_alarms() + unit_logic.update_alarms(self) -- if in auto mode, SCRAM on certain alarms - logic.update_auto_safety(public) + unit_logic.update_auto_safety(self, public) -- update status text - logic.update_status_text() + unit_logic.update_status_text(self) -- handle redstone I/O if #self.redstone > 0 then - logic.handle_redstone() + unit_logic.handle_redstone(self) elseif not self.plc_cache.rps_trip then self.em_cool_opened = false end diff --git a/supervisor/unit_logic.lua b/supervisor/unit_logic.lua index fd6a67f..db8af3a 100644 --- a/supervisor/unit_logic.lua +++ b/supervisor/unit_logic.lua @@ -31,13 +31,12 @@ local ALARM_LIMS = const.ALARM_LIMITS local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS local RS_THRESH = const.RS_THRESHOLDS -local self = nil ---@type _unit_self - ---@class unit_logic_extension local logic = {} -- update the annunciator -function logic.update_annunciator() +---@param self _unit_self +function logic.update_annunciator(self) local DT_KEYS = self.types.DT_KEYS local _get_dt = self._get_dt @@ -421,21 +420,23 @@ function logic.update_annunciator() end -- update an alarm state given conditions +---@param self _unit_self ---@param tripped boolean if the alarm condition is still active ---@param alarm alarm_def alarm table ---@return boolean new_trip if the alarm just changed to being tripped -local function _update_alarm_state(tripped, alarm) +local function _update_alarm_state(self, tripped, alarm) return alarm_ctl.update_alarm_state("UNIT " .. self.r_id, self.db.alarm_states, tripped, alarm) end -- evaluate alarm conditions -function logic.update_alarms() +---@param self _unit_self +function logic.update_alarms(self) local annunc = self.db.annunciator local plc_cache = self.plc_cache -- Containment Breach -- lost plc with critical damage (rip plc, you will be missed) - _update_alarm_state((not plc_cache.ok) and (plc_cache.damage > 99), self.alarms.ContainmentBreach) + _update_alarm_state(self, (not plc_cache.ok) and (plc_cache.damage > 99), self.alarms.ContainmentBreach) -- Containment Radiation local rad_alarm = false @@ -444,38 +445,38 @@ function logic.update_alarms() rad_alarm = self.last_radiation >= ALARM_LIMS.HIGH_RADIATION break end - _update_alarm_state(rad_alarm, self.alarms.ContainmentRadiation) + _update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation) -- Reactor Lost - _update_alarm_state(self.had_reactor and self.plc_i == nil, self.alarms.ReactorLost) + _update_alarm_state(self, self.had_reactor and self.plc_i == nil, self.alarms.ReactorLost) -- Critical Damage - _update_alarm_state(plc_cache.damage >= 100, self.alarms.CriticalDamage) + _update_alarm_state(self, plc_cache.damage >= 100, self.alarms.CriticalDamage) -- Reactor Damage local rps_dmg_90 = plc_cache.rps_status.high_dmg and not self.last_rps_trips.high_dmg - if _update_alarm_state((plc_cache.damage > 0) or rps_dmg_90, self.alarms.ReactorDamage) then + if _update_alarm_state(self, (plc_cache.damage > 0) or rps_dmg_90, self.alarms.ReactorDamage) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorDamage.id]," <<")) log.debug(util.c("| plc_cache.damage[", plc_cache.damage, "] rps_dmg_90[", rps_dmg_90, "]")) end -- Over-Temperature local rps_high_temp = plc_cache.rps_status.high_temp and not self.last_rps_trips.high_temp - if _update_alarm_state((plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) then + if _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorOverTemp.id]," <<")) log.debug(util.c("| plc_cache.temp[", plc_cache.temp, "] rps_high_temp[", rps_high_temp, "]")) end -- High Temperature local high_temp = math.min(math.max(self.plc_cache.high_temp_lim, 1100), 1199.995) - _update_alarm_state(plc_cache.temp >= high_temp, self.alarms.ReactorHighTemp) + _update_alarm_state(self, plc_cache.temp >= high_temp, self.alarms.ReactorHighTemp) -- Waste Leak - _update_alarm_state(plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) + _update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) -- High Waste local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste - if _update_alarm_state((plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) then + if _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorHighWaste.id]," <<")) log.debug(util.c("| plc_cache.waste[", plc_cache.waste, "] rps_high_waste[", rps_high_waste, "]")) end @@ -490,7 +491,7 @@ function logic.update_alarms() end end - _update_alarm_state(rps_alarm, self.alarms.RPSTransient) + _update_alarm_state(self, rps_alarm, self.alarms.RPSTransient) -- RCS Transient local any_low = annunc.CoolantLevelLow @@ -523,7 +524,7 @@ function logic.update_alarms() end end - if _update_alarm_state(rcs_trans, self.alarms.RCSTransient) then + if _update_alarm_state(self, rcs_trans, self.alarms.RCSTransient) then log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.RCSTransient.id]," <<")) log.debug(util.c("| any_low[", any_low, "] any_over[", any_over, "] gen_trip[", gen_trip, "]")) log.debug(util.c("| RCPTrip[", annunc.RCPTrip, "] MaxWaterReturnFeed[", annunc.MaxWaterReturnFeed, "]")) @@ -534,15 +535,16 @@ function logic.update_alarms() -- Turbine Trip local any_trip = false for i = 1, #annunc.TurbineTrip do any_trip = any_trip or annunc.TurbineTrip[i] end - _update_alarm_state(any_trip, self.alarms.TurbineTrip) + _update_alarm_state(self, any_trip, self.alarms.TurbineTrip) -- update last trips table for key, val in pairs(plc_cache.rps_status) do self.last_rps_trips[key] = val end end -- update the internal automatic safety control performed while in auto control mode +---@param self _unit_self ---@param public reactor_unit reactor unit public functions -function logic.update_auto_safety(public) +function logic.update_auto_safety(self, public) if self.auto_engaged then local alarmed = false @@ -569,7 +571,8 @@ function logic.update_auto_safety(public) end -- update the two unit status text messages -function logic.update_status_text() +---@param self _unit_self +function logic.update_status_text(self) local annunc = self.db.annunciator -- check if an alarm is active (tripped or ack'd) @@ -731,7 +734,8 @@ function logic.update_status_text() end -- handle unit redstone I/O -function logic.handle_redstone() +---@param self _unit_self +function logic.handle_redstone(self) local annunc = self.db.annunciator local cache = self.plc_cache local rps = cache.rps_status @@ -888,9 +892,4 @@ function logic.handle_redstone() end end --- link the self instance and return the logic interface ----@param unit_self _unit_self -return function (unit_self) - self = unit_self - return logic -end +return logic From 0a26629e20c9550469b44149d4b40bc3ef3bea40 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 30 Apr 2025 10:17:28 -0400 Subject: [PATCH 28/42] fixed no_ring_back going to RING_BACK if leaving TRIPPED not ACKED --- supervisor/alarm_ctl.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supervisor/alarm_ctl.lua b/supervisor/alarm_ctl.lua index d238fd9..13ba237 100644 --- a/supervisor/alarm_ctl.lua +++ b/supervisor/alarm_ctl.lua @@ -89,6 +89,9 @@ function alarm_ctl.update_alarm_state(caller_tag, alarm_states, tripped, alarm, -- was acked by coordinator alarm.state = AISTATE.ACKED end + elseif no_ring_back then + alarm.state = AISTATE.INACTIVE + alarm_states[alarm.id] = ALARM_STATE.INACTIVE else alarm.state = AISTATE.RING_BACK alarm_states[alarm.id] = ALARM_STATE.RING_BACK From 0debbdc167c7dd5929b5cdcadc138b7a6ca7b2d7 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 30 Apr 2025 10:25:30 -0400 Subject: [PATCH 29/42] fixed facility not scram'ing on radiation --- supervisor/facility_update.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index 62dd5fb..371700d 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -645,7 +645,7 @@ function update.auto_safety() end if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then - local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault + local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.radiation or astatus.gen_fault if scram and not self.ascram then -- SCRAM all units From 41b6a558d512d359b6e0cd8b5e1f7513b1fec487 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 5 May 2025 16:35:44 +0000 Subject: [PATCH 30/42] init RTU gateway UI after checking for modem to prevent that failure making a UI mess --- rtu/startup.lua | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 1b3d4ad..8ac188c 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -564,17 +564,6 @@ local function main() log.debug("boot> running sys_config()") if sys_config() then - -- start UI - local message - rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode) - - if not rtu_state.fp_ok then - println_ts(util.c("UI error: ", message)) - println("startup> running without front panel") - log.error(util.c("front panel GUI render failed with error ", message)) - log.info("startup> running in headless mode without front panel") - end - -- check modem if smem_dev.modem == nil then println("startup> wireless modem not found") @@ -596,6 +585,17 @@ local function main() databus.tx_hw_spkr_count(#smem_dev.sounders) + -- start UI + local message + rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode) + + if not rtu_state.fp_ok then + println_ts(util.c("UI error: ", message)) + println("startup> running without front panel") + log.error(util.c("front panel GUI render failed with error ", message)) + log.info("startup> running in headless mode without front panel") + end + -- start connection watchdog smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) log.debug("startup> conn watchdog started") From e3dbda3c54372df485668cc9cea789baf11efdb5 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 10:42:52 -0400 Subject: [PATCH 31/42] fixed logic for duplicate input detection --- rtu/config/redstone.lua | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 854d6b9..d8fd56f 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -185,15 +185,6 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function new_rs(port) - if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then - for i = 1, #tmp_cfg.Redstone do - if tmp_cfg.Redstone[i].port == port then - rs_pane.set_value(6) - return - end - end - end - self.rs_cfg_editing = false local text @@ -316,8 +307,16 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) port = port, side = side_options_map[side.get_value()], color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil), - invert = self.rs_cfg_inverted.get_value() or nil - } + + -- check for duplicate inputs for this unit/facility + if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then + for i = 1, #tmp_cfg.Redstone do + if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then + rs_pane.set_value(7) + return + end + end + end if self.rs_cfg_editing == false then table.insert(tmp_cfg.Redstone, def) From 12ead136a30646f031103f20ee8462f30dfc2f8a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 11:27:53 -0400 Subject: [PATCH 32/42] #604 configuration of redstone RTUs --- rtu/config/redstone.lua | 285 ++++++++++++++++++++-------- rtu/configure.lua | 6 +- supervisor/session/rtu/redstone.lua | 1 - 3 files changed, 208 insertions(+), 84 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index d8fd56f..53fd9fd 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -1,4 +1,5 @@ local constants = require("scada-common.constants") +local ppm = require("scada-common.ppm") local rsio = require("scada-common.rsio") local util = require("scada-common.util") @@ -34,6 +35,7 @@ local IO_MODE = rsio.IO_MODE local LEFT = core.ALIGN.LEFT local self = { + rs_cfg_phy = false, ---@type string|nil|false rs_cfg_port = 1, ---@type IO_PORT rs_cfg_editing = false, ---@type integer|false @@ -109,6 +111,23 @@ local function color_to_idx(color) end end +-- select the subset of redstone entries assigned to the given phy +---@param cfg rtu_rs_definition[] the full redstone entry list +---@param phy string|nil which phy to get redstone entries for +---@param invert boolean? true to get all except this phy +---@return rtu_rs_definition[] +local function redstone_subset(cfg, phy, invert) + local subset = {} + + for i = 1, #cfg do + if ((not invert) and cfg[i].relay == phy) or (invert and cfg[i].relay ~= phy) then + table.insert(subset, cfg[i]) + end + end + + return subset +end + local redstone = {} -- validate a redstone entry @@ -138,21 +157,89 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) --#region Redstone - local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_9 = Div{parent=rs_cfg,x=2,y=4,width=49} + local rs_c_10 = Div{parent=rs_cfg,x=2,y=4,width=49} - local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8}} + local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7,rs_c_8,rs_c_9,rs_c_10}} - TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} + local header = TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} - TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} - local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + --#region Interface Selection + + TextBox{parent=rs_c_1,x=1,y=1,text="Configure this computer or a redstone relay."} + local iface_list = ListBox{parent=rs_c_1,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + -- update relay interface list + function tool_ctl.update_relay_list() + local mounts = ppm.list_mounts() + + iface_list.remove_all() + + -- assemble list of configured relays + local relays = {} + for i = 1, #tmp_cfg.Redstone do + local def = tmp_cfg.Redstone[i] + if def.relay and not util.table_contains(relays, def.relay) then + table.insert(relays, def.relay) + end + end + + -- add unconfigured connected relays + for name, entry in pairs(mounts) do + if entry.type == "redstone_relay" and not util.table_contains(relays, name) then + table.insert(relays, name) + end + end + + local function config_rs(name) + header.set_value(" Redstone Connections (" .. name .. ")") + + self.rs_cfg_phy = tri(name == "local", nil, name) + + tool_ctl.gen_rs_summary() + rs_pane.set_value(2) + end + + local line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=line,x=1,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=line,x=3,y=2,text="This Computer",fg_bg=cpair(colors.gray,colors.white)} + local count = #redstone_subset(ini_cfg.Redstone, nil) + TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)} + + PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs("local")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + + for i = 1, #relays do + local name = relays[i] + + line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=line,x=1,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=line,x=3,y=2,text="Redstone Relay",fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=line,x=18,y=2,text=tri(mounts[name],"ONLINE","OFFLINE"),fg_bg=cpair(tri(mounts[name],colors.green,colors.red),colors.white)} + count = #redstone_subset(ini_cfg.Redstone, name) + TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)} + + PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + end + end + + tool_ctl.update_relay_list() + + PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_1,x=27,y=14,min_width=23,text="I don't see my relay!",callback=function()rs_pane.set_value(10)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg} + + --#endregion + --#region Configuration List + + TextBox{parent=rs_c_2,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} + local rs_list = ListBox{parent=rs_c_2,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function rs_revert() tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) @@ -160,29 +247,44 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) end local function rs_apply() - settings.set("Redstone", tmp_cfg.Redstone) + -- add the changed data to the existing saved data + local new_data = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy) + local new_save = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy, true) + for i = 1, #new_data do table.insert(new_save, new_data[i]) end + + settings.set("Redstone", new_save) if settings.save("/rtu.settings") then load_settings(settings_cfg, true) load_settings(ini_cfg) - rs_pane.set_value(4) + rs_pane.set_value(5) -- for return to list from saved screen + -- this will delete unsaved changes for other phy's, which is acceptable tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) tool_ctl.gen_rs_summary() else - rs_pane.set_value(5) + rs_pane.set_value(6) end end - PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - local rs_revert_btn = PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} - PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + local function rs_back() + self.rs_cfg_phy = false + rs_pane.set_value(1) + header.set_value(" Redstone Connections") + end - TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."} + PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + local rs_revert_btn = PushButton{parent=rs_c_2,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=rs_c_2,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + local rs_apply_btn = PushButton{parent=rs_c_2,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} - local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + --#endregion + --#region Port Selection + + TextBox{parent=rs_c_3,x=1,y=1,text="Select one of the below ports to use."} + + local rs_ports = ListBox{parent=rs_c_3,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function new_rs(port) self.rs_cfg_editing = false @@ -241,7 +343,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) self.rs_cfg_selection.set_value(text) self.rs_cfg_port = port - rs_pane.set_value(3) + rs_pane.set_value(4) end -- add entries to redstone option list @@ -262,38 +364,43 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)} end - PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - self.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""} + --#endregion + --#region Port Configuration - PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.rs_cfg_selection = TextBox{parent=rs_c_4,x=1,y=1,height=2,text=""} - self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"} - local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} + PushButton{parent=rs_c_4,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - self.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"} - self.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} + self.rs_cfg_side_l = TextBox{parent=rs_c_4,x=1,y=4,width=11,text="Output Side"} + local side = Radio2D{parent=rs_c_4,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} + + self.rs_cfg_unit_l = TextBox{parent=rs_c_4,x=25,y=7,width=7,text="Unit ID"} + self.rs_cfg_unit = NumberField{parent=rs_c_4,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} local function set_bundled(bundled) if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end end - self.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."} + self.rs_cfg_shortcut = TextBox{parent=rs_c_4,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."} self.rs_cfg_shortcut.hide(true) - self.rs_cfg_bundled = Checkbox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg} - self.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} + self.rs_cfg_bundled = Checkbox{parent=rs_c_4,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg} + self.rs_cfg_color = Radio2D{parent=rs_c_4,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} self.rs_cfg_color.disable() - local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + local rs_err = TextBox{parent=rs_c_4,x=8,y=14,width=30,text="Unit ID invalid.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} rs_err.hide(true) local function back_from_rs_opts() rs_err.hide(true) - if self.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end + if self.rs_cfg_editing ~= false then rs_pane.set_value(2) else rs_pane.set_value(3) end end local function save_rs_entry() + assert(self.rs_cfg_phy ~= false, "tried to save a redstone entry without a phy") + local port = self.rs_cfg_port local u = tonumber(self.rs_cfg_unit.get_value()) @@ -307,18 +414,21 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) port = port, side = side_options_map[side.get_value()], color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil), - - -- check for duplicate inputs for this unit/facility - if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then - for i = 1, #tmp_cfg.Redstone do - if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then - rs_pane.set_value(7) - return - end - end - end + invert = self.rs_cfg_inverted.get_value() or nil, + relay = self.rs_cfg_phy + } if self.rs_cfg_editing == false then + -- check for duplicate inputs for this unit/facility + if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then + for i = 1, #tmp_cfg.Redstone do + if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then + rs_pane.set_value(7) + return + end + end + end + table.insert(tmp_cfg.Redstone, def) else def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port @@ -332,12 +442,13 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), port = IO.WASTE_PU + i, side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), - color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil) + color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil), + relay = self.rs_cfg_phy }) end end - rs_pane.set_value(1) + rs_pane.set_value(2) tool_ctl.gen_rs_summary() side.set_value(1) @@ -349,30 +460,35 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) else rs_err.show() end end - PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - self.rs_cfg_advanced = PushButton{parent=rs_c_3,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(8)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} - PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + self.rs_cfg_advanced = PushButton{parent=rs_c_4,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(9)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=rs_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"} - PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + --#endregion - TextBox{parent=rs_c_5,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} - PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + TextBox{parent=rs_c_5,x=1,y=1,text="Settings saved!"} + PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} + TextBox{parent=rs_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=rs_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} - TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"} - TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"} + TextBox{parent=rs_c_7,x=1,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."} PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - TextBox{parent=rs_c_8,x=1,y=1,height=5,text="Advanced Options"} - self.rs_cfg_inverted = Checkbox{parent=rs_c_8,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg} - TextBox{parent=rs_c_8,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)} - PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + TextBox{parent=rs_c_8,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"} + TextBox{parent=rs_c_8,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"} + TextBox{parent=rs_c_8,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"} + PushButton{parent=rs_c_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_9,x=1,y=1,height=5,text="Advanced Options"} + self.rs_cfg_inverted = Checkbox{parent=rs_c_9,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg} + TextBox{parent=rs_c_9,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)} + PushButton{parent=rs_c_9,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=rs_c_10,x=1,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."} + PushButton{parent=rs_c_10,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} --#endregion @@ -422,7 +538,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) side.set_value(side_to_idx(def.side)) self.rs_cfg_color.set_value(value) self.rs_cfg_inverted.set_value(def.invert or false) - rs_pane.set_value(3) + rs_pane.set_value(4) end local function delete_rs_entry(idx) @@ -432,34 +548,41 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) -- generate the redstone summary list function tool_ctl.gen_rs_summary() + assert(self.rs_cfg_phy ~= false, "tried to generate a summary without a phy set") + rs_list.remove_all() - local modified = #ini_cfg.Redstone ~= #tmp_cfg.Redstone + local ini = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy) + local tmp = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy) + + local modified = #ini ~= #tmp for i = 1, #tmp_cfg.Redstone do local def = tmp_cfg.Redstone[i] - local name = rsio.to_string(def.port) - local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") - local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple) - local conn = def.side - local unit = util.strval(def.unit or "F") + if def.relay == self.rs_cfg_phy then + local name = rsio.to_string(def.port) + local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") + local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple) + local conn = def.side + local unit = util.strval(def.unit or "F") - if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end + if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end - local entry = Div{parent=rs_list,height=1} - TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)} - TextBox{parent=entry,x=2,y=1,width=14,text=name} - TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} - TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} - PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} + local entry = Div{parent=rs_list,height=1} + TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)} + TextBox{parent=entry,x=2,y=1,width=14,text=name} + TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} + PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} - if not modified then - local a = ini_cfg.Redstone[i] - local b = tmp_cfg.Redstone[i] + if not modified then + local a = ini_cfg.Redstone[i] + local b = tmp_cfg.Redstone[i] - modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert) + modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert) + end end end diff --git a/rtu/configure.lua b/rtu/configure.lua index 03fb1b9..380f4ca 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -76,6 +76,7 @@ local tool_ctl = { gen_summary = nil, ---@type function load_legacy = nil, ---@type function update_peri_list = nil, ---@type function + update_relay_list = nil, ---@type function gen_peri_summary = nil, ---@type function gen_rs_summary = nil, ---@type function } @@ -128,7 +129,7 @@ end ---@param data rtu_rs_definition[] function tool_ctl.deep_copy_rs(data) local array = {} - for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color, invert = d.invert }) end + for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color, invert = d.invert, relay = d.relay }) end return array end @@ -208,7 +209,6 @@ local function config_view(display) end local function show_rs_conns() - tool_ctl.gen_rs_summary() main_pane.set_value(9) end @@ -348,10 +348,12 @@ function configurator.configure(ask_config) ---@diagnostic disable-next-line: discard-returns ppm.handle_unmount(param1) tool_ctl.update_peri_list() + tool_ctl.update_relay_list() elseif event == "peripheral" then ---@diagnostic disable-next-line: discard-returns ppm.mount(param1) tool_ctl.update_peri_list() + tool_ctl.update_relay_list() end if event == "terminate" then return end diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 610432d..7b91dc3 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -10,7 +10,6 @@ local redstone = {} local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local MODBUS_FCODE = types.MODBUS_FCODE -local IO_PORT = rsio.IO local IO_LVL = rsio.IO_LVL local IO_MODE = rsio.IO_MODE From 7404e6da311f2169e3e0f63b16aa729d9e9bae8a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 11:48:32 -0400 Subject: [PATCH 33/42] #604 updated self check for relays and added duplicate input detection --- rtu/config/check.lua | 47 +++++++++++++++++++++++++++++++++----------- rtu/configure.lua | 3 ++- rtu/startup.lua | 8 ++++---- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/rtu/config/check.lua b/rtu/config/check.lua index 6818ba6..bc3f6d5 100644 --- a/rtu/config/check.lua +++ b/rtu/config/check.lua @@ -136,22 +136,45 @@ local function self_check() self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones") -- check redstone configurations - local ifaces = {} - local bundled_sides = {} + + local phys = {} ---@type rtu_rs_definition[][] + local inputs = { [0] = {}, {}, {}, {}, {} } + for i = 1, #cfg.Redstone do local entry = cfg.Redstone[i] - local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "") - local dupe = util.table_contains(ifaces, ident) - local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil)) + local name = entry.relay or "local" - local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is") + if phys[name] == nil then phys[name] = {} end + table.insert(phys[entry.relay or "local"], entry) + end - self.self_check_msg("> check redstone " .. ident .. " unique...", not dupe, "only one port should be set to a side/color combination") - self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work") - self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry") + for name, entries in pairs(phys) do + TextBox{parent=self.sc_log,text="> checking redstone @ "..name.."...",fg_bg=cpair(colors.blue,colors.white)} - bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil - table.insert(ifaces, ident) + local ifaces = {} + local bundled_sides = {} + + for i = 1, #entries do + local entry = entries[i] + local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "") + + local sc_dupe = util.table_contains(ifaces, ident) + local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil)) + + local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is") + + self.self_check_msg("> check redstone " .. ident .. " unique...", not sc_dupe, "only one port should be set to a side/color combination") + self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work") + self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry") + + if rsio.get_io_dir(entry.port) == rsio.IO_DIR.IN then + local in_dupe = util.table_contains(inputs[entry.unit or 0], entry.port) + self.self_check_msg("> check redstone " .. ident .. " input...", not in_dupe, "you cannot have multiple of the same input for a given unit or the facility ("..rsio.to_string(entry.port)..")") + end + + bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil + table.insert(ifaces, ident) + end end -- check peripheral configurations @@ -245,7 +268,7 @@ function check.create(main_pane, settings_cfg, check_sys, style) TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg} - self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=500,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local last_check = { nil, nil } diff --git a/rtu/configure.lua b/rtu/configure.lua index 380f4ca..eea6072 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -36,7 +36,8 @@ local changes = { { "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }, { "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } }, - { "v1.11.8", { "Added advanced option to invert digital redstone signals" } } + { "v1.11.8", { "Added advanced option to invert digital redstone signals" } }, + { "v1.12.0", { "Added support for redstone relays" } } } ---@class rtu_configurator diff --git a/rtu/startup.lua b/rtu/startup.lua index 8ac188c..314c2c7 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.11.8" +local RTU_VERSION = "v1.12.0" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_HW_STATE = databus.RTU_HW_STATE @@ -152,7 +152,7 @@ local function main() --#region Redstone Interfaces local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[] - local all_conns = {} + local all_conns = { [0] = {}, {}, {}, {}, {} } -- go through redstone definitions list for entry_idx = 1, #rtu_redstone do @@ -197,11 +197,11 @@ local function main() log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay")) end - rs_rtus[entry.relay] = { name = entry.relay, rtu = redstone_rtu.new(relay), phy = relay, banks = {} } + rs_rtus[entry.relay] = { name = entry.relay, 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", rtu = redstone_rtu.new(), phy = rs, banks = {} } + rs_rtus[0] = { name = "redstone_local", rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } } end -- verify configuration From 8eff1c0d761e0f002065b89b7f5ce7cb5a7f20a7 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 20:03:20 -0400 Subject: [PATCH 34/42] #604 refresh connections count on saving an interface --- rtu/config/redstone.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 53fd9fd..7592bf9 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -263,6 +263,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) -- this will delete unsaved changes for other phy's, which is acceptable tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) tool_ctl.gen_rs_summary() + tool_ctl.update_relay_list() else rs_pane.set_value(6) end From 069a7ce0ad0da1f74e86606396c1b5ea136e1deb Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 20:03:48 -0400 Subject: [PATCH 35/42] #604 front panel updates and hw state tracking fixes --- rtu/panel/front_panel.lua | 30 +++++++++++++++++++++++------- rtu/startup.lua | 15 ++++++++------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index 51adf30..02f5979 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -19,7 +19,8 @@ local LED = require("graphics.elements.indicators.LED") local LEDPair = require("graphics.elements.indicators.LEDPair") local RGBLED = require("graphics.elements.indicators.RGBLED") -local LINK_STATE = types.PANEL_LINK_STATE +local LINK_STATE = types.PANEL_LINK_STATE +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local ALIGN = core.ALIGN @@ -129,30 +130,45 @@ local function init(panel, units) -- show routine statuses for i = 1, list_length do TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)} - local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn} + local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=util.trinary(units[i].type~=RTU_UNIT_TYPE.REDSTONE,ind_grn,cpair(style.ind_bkg,style.ind_bkg))} rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update) end local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3} + local relay_counter = 0 + -- show hardware statuses for i = 1, list_length do local unit = units[i] + local is_rs = unit.type == RTU_UNIT_TYPE.REDSTONE + -- hardware status local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}} unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update) -- unit name identifier (type + index) - local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end - local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15} + local function get_name() + if is_rs then + local is_local = unit.name == "redstone_local" + relay_counter = relay_counter + util.trinary(is_local, 0, 1) + return util.c("REDSTONE", util.trinary(is_local, "", " RELAY "..relay_counter)) + else + return util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) + end + end - name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end) + local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(),width=util.trinary(is_rs,24,15)} + + name_box.register(databus.ps, "unit_type_" .. i, function () name_box.set_value(get_name()) end) -- assignment (unit # or facility) - local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) - TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg} + if unit.reactor then + local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) + TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg} + end end end diff --git a/rtu/startup.lua b/rtu/startup.lua index 314c2c7..69e3a56 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -151,7 +151,7 @@ local function main() local function sys_config() --#region Redstone Interfaces - local rs_rtus = {} ---@type { name: string, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[] + 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 @@ -187,21 +187,24 @@ local function main() elseif not rs_rtus[entry.relay] then log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay)) - local relay = ppm.get_device(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, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } } + 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", rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } } + rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } } end -- verify configuration @@ -291,8 +294,6 @@ local function main() end end - local hw_state = util.trinary(def.phy, RTU_HW_STATE.OK, RTU_HW_STATE.OFFLINE) - ---@type rtu_registry_entry local unit = { uid = 0, @@ -304,7 +305,7 @@ local function main() rs_conns = rtu_conns, is_multiblock = false, formed = nil, - hw_state = hw_state, + hw_state = def.hw_state, rtu = def.rtu, modbus_io = modbus.new(def.rtu, false), pkt_queue = nil, From 8f6425b81439185dec90b9a0cab73a660cd087f4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 20:04:39 -0400 Subject: [PATCH 36/42] #604 fixed supervisor bugs with new redstone --- supervisor/session/rtu.lua | 16 +++++++++------- supervisor/session/rtu/redstone.lua | 13 +++++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 4f04a1e..dea4eb2 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -140,13 +140,15 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad -- link this to any subsystems this RTU provides connections for if type(unit) ~= "nil" then - for assignment, _ in pairs(unit_advert.rs_conns) do - if assignment == 0 then - facility.add_redstone(unit) - elseif assignment > 0 and assignment < #self.fac_units then - self.fac_units[assignment].add_redstone(unit) - else - log.warning(util.c(log_tag, "_handle_advertisement(): unrecognized redstone RTU assignment ", assignment, " ", type_string)) + for assignment, conns in pairs(unit_advert.rs_conns) do + if #conns > 0 then + if assignment == 0 then + facility.add_redstone(unit) + elseif assignment > 0 and assignment <= #self.fac_units then + self.fac_units[assignment].add_redstone(unit) + else + log.warning(util.c(log_tag, "_handle_advertisement(): unrecognized redstone RTU assignment ", assignment, " ", type_string)) + end end end end diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 7b91dc3..2cd2856 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -127,7 +127,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) write = function () end } - self.db.io[port] = io_f + self.db.io[bank][port] = io_f elseif mode == IO_MODE.DIGITAL_OUT then self.has_do = true table.insert(self.io_map.digital_out, io_entry) @@ -145,7 +145,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) end } - self.db.io[port] = io_f + self.db.io[bank][port] = io_f elseif mode == IO_MODE.ANALOG_IN then self.has_ai = true table.insert(self.io_map.analog_in, io_entry) @@ -160,7 +160,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) write = function () end } - self.db.io[port] = io_f + self.db.io[bank][port] = io_f elseif mode == IO_MODE.ANALOG_OUT then self.has_ao = true table.insert(self.io_map.analog_out, io_entry) @@ -180,7 +180,7 @@ function redstone.new(session_id, unit_id, advert, out_queue) end } - self.db.io[port] = io_f + self.db.io[bank][port] = io_f else -- should be unreachable code, we already validated ports log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true) @@ -396,9 +396,10 @@ function redstone.new(session_id, unit_id, advert, out_queue) self.session.post_update() end - -- invalidate build cache + -- force a re-read of cached outputs function public.invalidate_cache() - -- no build cache for this device + _read_coils() + _read_holding_registers() end -- get the unit session database From f01fb6286322d631734624682eff2f29c878db73 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 20:05:03 -0400 Subject: [PATCH 37/42] #604 updated emergency coolant annunciator logic --- supervisor/unit_logic.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/supervisor/unit_logic.lua b/supervisor/unit_logic.lua index db8af3a..631449f 100644 --- a/supervisor/unit_logic.lua +++ b/supervisor/unit_logic.lua @@ -173,12 +173,8 @@ function logic.update_annunciator(self) annunc.EmergencyCoolant = 1 - for i = 1, #self.redstone do - local io = self.redstone[i].get_db().io[IO.U_EMER_COOL] - if io ~= nil then - annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2) - break - end + if self.io_ctl.is_connected(IO.U_EMER_COOL) then + annunc.EmergencyCoolant = util.trinary(self.io_ctl.digital_read(IO.U_EMER_COOL), 3, 2) end --#endregion From 41e6d89a4bf1768c5e54487b126cb83e18cb7fd9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 7 May 2025 20:06:04 -0400 Subject: [PATCH 38/42] incremented comms version for RTU advertisement changes --- scada-common/comms.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 48cf416..f827ab8 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,7 +17,7 @@ local max_distance = nil local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) -comms.version = "3.0.5" +comms.version = "3.0.6" comms.api_version = "0.0.9" ---@enum PROTOCOL From 5f8c947105feb91111fa56ae8e87a8063c649fa9 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 9 May 2025 15:41:14 +0000 Subject: [PATCH 39/42] cleanup and fixes --- rtu/config/redstone.lua | 10 +++++----- rtu/configure.lua | 2 +- rtu/panel/front_panel.lua | 2 +- rtu/startup.lua | 1 - supervisor/session/rtu.lua | 9 +++------ 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 7592bf9..3227f3f 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -413,10 +413,10 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) local def = { unit = tri(PORT_DSGN[port] == 1, u, nil), port = port, + relay = self.rs_cfg_phy, side = side_options_map[side.get_value()], color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil), - invert = self.rs_cfg_inverted.get_value() or nil, - relay = self.rs_cfg_phy + invert = self.rs_cfg_inverted.get_value() or nil } if self.rs_cfg_editing == false then @@ -442,9 +442,9 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) table.insert(tmp_cfg.Redstone, { unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), port = IO.WASTE_PU + i, + relay = self.rs_cfg_phy, side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), - color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil), - relay = self.rs_cfg_phy + color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil) }) end end @@ -582,7 +582,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) local a = ini_cfg.Redstone[i] local b = tmp_cfg.Redstone[i] - modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert) + modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.relay ~= b.relay) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert) end end end diff --git a/rtu/configure.lua b/rtu/configure.lua index eea6072..022dfbb 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -130,7 +130,7 @@ end ---@param data rtu_rs_definition[] function tool_ctl.deep_copy_rs(data) local array = {} - for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color, invert = d.invert, relay = d.relay }) end + for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, relay = d.relay, side = d.side, color = d.color, invert = d.invert }) end return array end diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index 02f5979..0b9c2ed 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -154,7 +154,7 @@ local function init(panel, units) if is_rs then local is_local = unit.name == "redstone_local" relay_counter = relay_counter + util.trinary(is_local, 0, 1) - return util.c("REDSTONE", util.trinary(is_local, "", " RELAY "..relay_counter)) + return util.c("REDSTONE", util.trinary(is_local, "", " RELAY " .. relay_counter)) else return util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end diff --git a/rtu/startup.lua b/rtu/startup.lua index 69e3a56..f4e9eed 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -213,7 +213,6 @@ local function main() valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color)) end - -- local rs_rtu = rs_rtus[phy].rtu local bank = rs_rtus[phy].banks[for_reactor] local conns = all_conns[for_reactor] diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index dea4eb2..094c13d 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -104,14 +104,11 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false)) advert_validator.assert_type_int(unit_advert.reactor) - if u_type == RTU_UNIT_TYPE.REDSTONE then - advert_validator.assert_type_table(unit_advert.rs_conns) - end - if advert_validator.valid() then if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end - if unit_advert.reactor == -1 then + if (unit_advert.reactor == -1) or (u_type == RTU_UNIT_TYPE.REDSTONE) then + advert_validator.assert((unit_advert.reactor == -1) and (u_type == RTU_UNIT_TYPE.REDSTONE)) advert_validator.assert_type_table(unit_advert.rs_conns) else advert_validator.assert_min(unit_advert.reactor, 0) @@ -147,7 +144,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad elseif assignment > 0 and assignment <= #self.fac_units then self.fac_units[assignment].add_redstone(unit) else - log.warning(util.c(log_tag, "_handle_advertisement(): unrecognized redstone RTU assignment ", assignment, " ", type_string)) + log.warning(util.c(log_tag, "_handle_advertisement(): invalid redstone RTU assignment ", assignment)) end end end From 35f82af2e2497fea1205e2e9df6a8f37daba7d13 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 10 May 2025 11:25:52 -0400 Subject: [PATCH 40/42] fixed CONFIG button coloring --- rtu/config/redstone.lua | 2 +- rtu/startup.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rtu/config/redstone.lua b/rtu/config/redstone.lua index 3227f3f..74506f3 100644 --- a/rtu/config/redstone.lua +++ b/rtu/config/redstone.lua @@ -226,7 +226,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style) count = #redstone_subset(ini_cfg.Redstone, name) TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)} - PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} + PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} end end diff --git a/rtu/startup.lua b/rtu/startup.lua index f4e9eed..d3812c6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.12.0" +local RTU_VERSION = "v1.12.1" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_HW_STATE = databus.RTU_HW_STATE From fcb17ae5e79520ed47811ad90cc2823d597af00b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 10 May 2025 11:26:19 -0400 Subject: [PATCH 41/42] show "new!" next to new config fields --- coordinator/config/hmi.lua | 1 + coordinator/startup.lua | 2 +- pocket/config/system.lua | 1 + pocket/startup.lua | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/coordinator/config/hmi.lua b/coordinator/config/hmi.lua index 80c67fe..6f52cf1 100644 --- a/coordinator/config/hmi.lua +++ b/coordinator/config/hmi.lua @@ -240,6 +240,7 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style) 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} TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"} + TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"} diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 323af98..cd1213c 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.6.14" +local COORDINATOR_VERSION = "v1.6.15" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/pocket/config/system.lua b/pocket/config/system.lua index 110a6cf..c3251b1 100644 --- a/pocket/config/system.lua +++ b/pocket/config/system.lua @@ -62,6 +62,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit) TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."} TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"} + TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} 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} diff --git a/pocket/startup.lua b/pocket/startup.lua index a2eddd6..dbebafb 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -22,7 +22,7 @@ local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") local threads = require("pocket.threads") -local POCKET_VERSION = "v0.13.2-beta" +local POCKET_VERSION = "v0.13.3-beta" local println = util.println local println_ts = util.println_ts From 264edc0030be9a333be8576e02524aa0ceedc856 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 10 May 2025 17:30:54 -0400 Subject: [PATCH 42/42] #592 fixed bug with pocket help page linking navigation --- pocket/iorx.lua | 2 +- pocket/pocket.lua | 8 +++++--- pocket/startup.lua | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pocket/iorx.lua b/pocket/iorx.lua index 76fcd9b..cdb5d91 100644 --- a/pocket/iorx.lua +++ b/pocket/iorx.lua @@ -314,7 +314,7 @@ function iorx.record_unit_data(data) local function blue(text) return { text = text, color = colors.blue } end -- if unit.reactor_data.rps_status then - -- for k, v in pairs(unit.alarms) do + -- for k, _ in pairs(unit.alarms) do -- unit.alarms[k] = ALARM_STATE.TRIPPED -- end -- end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index e0e285c..55cc829 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -269,8 +269,8 @@ function pocket.init_nav(smem) -- open an app ---@param app_id POCKET_APP_ID - ---@param on_loaded? function - function nav.open_app(app_id, on_loaded) + ---@param on_ready? function + function nav.open_app(app_id, on_ready) -- reset help return on navigating out of an app if app_id == APP_ID.ROOT then self.help_return = nil end @@ -283,7 +283,7 @@ function pocket.init_nav(smem) app = self.apps[app_id] else self.loader_return = nil end - if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_loaded }) end + if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) end self.cur_app = app_id self.pane.set_value(app_id) @@ -291,6 +291,8 @@ function pocket.init_nav(smem) if #app.sidebar_items > 0 then self.sidebar.update(app.sidebar_items) end + + if app.loaded and on_ready then on_ready() end else log.debug("tried to open unknown app") end diff --git a/pocket/startup.lua b/pocket/startup.lua index dbebafb..71e2a73 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -22,7 +22,7 @@ local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") local threads = require("pocket.threads") -local POCKET_VERSION = "v0.13.3-beta" +local POCKET_VERSION = "v0.13.4-beta" local println = util.println local println_ts = util.println_ts