From d9efd5b8d2851f353916218dcad39c2e6fe5c855 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 20 Apr 2024 16:32:18 -0400 Subject: [PATCH 1/8] #412 updates to RSIO for induction matrix low, high, and analog charge level --- rtu/configure.lua | 75 ++++++------ rtu/startup.lua | 2 +- scada-common/constants.lua | 12 ++ scada-common/rsio.lua | 214 ++++++++++++++++------------------- scada-common/util.lua | 2 +- supervisor/facility.lua | 14 ++- supervisor/session/rsctl.lua | 25 +++- supervisor/startup.lua | 2 +- test/rstest.lua | 79 ++++++------- 9 files changed, 226 insertions(+), 199 deletions(-) diff --git a/rtu/configure.lua b/rtu/configure.lua index 7758dc2..5dc440a 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -2,6 +2,7 @@ -- Configuration GUI -- +local constants = require("scada-common.constants") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local rsio = require("scada-common.rsio") @@ -39,39 +40,42 @@ local CENTER = core.ALIGN.CENTER local RIGHT = core.ALIGN.RIGHT -- rsio port descriptions -local PORT_DESC = { - "Facility SCRAM", - "Facility Acknowledge", - "Reactor SCRAM", - "Reactor RPS Reset", - "Reactor Enable", - "Unit Acknowledge", - "Facility Alarm (high prio)", - "Facility Alarm (any)", - "Waste Plutonium Valve", - "Waste Polonium Valve", - "Waste Po Pellets Valve", - "Waste Antimatter Valve", - "Reactor Active", - "Reactor in Auto Control", - "RPS Tripped", - "RPS Auto SCRAM", - "RPS High Damage", - "RPS High Temperature", - "RPS Low Coolant", - "RPS Excess Heated Coolant", - "RPS Excess Waste", - "RPS Insufficient Fuel", - "RPS PLC Fault", - "RPS Supervisor Timeout", - "Unit Alarm", - "Unit Emergency Cool. Valve" +local PORT_DESC_MAP = { + { IO.F_SCRAM, "Facility SCRAM" }, + { IO.F_ACK, "Facility Acknowledge" }, + { IO.R_SCRAM, "Reactor SCRAM" }, + { IO.R_RESET, "Reactor RPS Reset" }, + { IO.R_ENABLE, "Reactor Enable" }, + { IO.U_ACK, "Unit Acknowledge" }, + { IO.F_ALARM, "Facility Alarm (high prio)" }, + { IO.F_ALARM_ANY, "Facility Alarm (any)" }, + { IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" }, + { IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" }, + { IO.F_MATRIX_CHG, "Induction Matrix Charge %" }, + { IO.WASTE_PU, "Waste Plutonium Valve" }, + { IO.WASTE_PO, "Waste Polonium Valve" }, + { IO.WASTE_POPL, "Waste Po Pellets Valve" }, + { IO.WASTE_AM, "Waste Antimatter Valve" }, + { IO.R_ACTIVE, "Reactor Active" }, + { IO.R_AUTO_CTRL, "Reactor in Auto Control" }, + { IO.R_SCRAMMED, "RPS Tripped" }, + { IO.R_AUTO_SCRAM, "RPS Auto SCRAM" }, + { IO.R_HIGH_DMG, "RPS High Damage" }, + { IO.R_HIGH_TEMP, "RPS High Temperature" }, + { IO.R_LOW_COOLANT, "RPS Low Coolant" }, + { IO.R_EXCESS_HC, "RPS Excess Heated Coolant" }, + { IO.R_EXCESS_WS, "RPS Excess Waste" }, + { IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" }, + { IO.R_PLC_FAULT, "RPS PLC Fault" }, + { IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" }, + { IO.U_ALARM, "Unit Alarm" }, + { IO.U_EMER_COOL, "Unit Emergency Cool. Valve" } } -- designation (0 = facility, 1 = unit) -local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } +local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 } -assert(#PORT_DESC == rsio.NUM_PORTS) +assert(#PORT_DESC_MAP == rsio.NUM_PORTS) assert(#PORT_DSGN == rsio.NUM_PORTS) -- changes to the config data/format to let the user know @@ -1167,14 +1171,17 @@ local function config_view(display) PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)} TextBox{parent=all_w_macro,x=16,y=1,width=5,height=1,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=all_w_macro,x=22,y=1,height=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)} + for i = 1, rsio.NUM_PORTS do - local name = rsio.to_string(i) - local io_dir = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, "[in]", "[out]") - local btn_color = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) + local p = PORT_DESC_MAP[i][1] + local name = rsio.to_string(p) + local io_dir = util.trinary(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]") + local btn_color = util.trinary(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) + local entry = Div{parent=rs_ports,height=1} - PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(i)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} + PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} TextBox{parent=entry,x=16,y=1,width=5,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC[i],fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=entry,x=22,y=1,height=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} diff --git a/rtu/startup.lua b/rtu/startup.lua index 9470124..027549c 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.9.4" +local RTU_VERSION = "v1.9.5" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE diff --git a/scada-common/constants.lua b/scada-common/constants.lua index 20925bd..678ea98 100644 --- a/scada-common/constants.lua +++ b/scada-common/constants.lua @@ -66,6 +66,18 @@ constants.ALARM_LIMITS = alarms --#endregion +--#region Supervisor Redstone Activation Thresholds + +---@class _rs_threshold_constants +local rs = {} + +rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW +rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH + +constants.RS_THRESHOLDS = rs + +--#endregion + --#region Supervisor Constants -- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 6d1e688..8f4c558 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -52,6 +52,8 @@ local IO_PORT = { -- facility F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm) F_ALARM_ANY = 8, -- active high, any alarm regardless of priority + F_MATRIX_LOW = 27, -- active high, induction matrix charge less than + F_MATRIX_HIGH = 28, -- active high, induction matrix charge high -- waste WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route @@ -75,17 +77,27 @@ local IO_PORT = { -- unit outputs U_ALARM = 25, -- active high, unit alarm - U_EMER_COOL = 26 -- active low, emergency coolant control + U_EMER_COOL = 26, -- active low, emergency coolant control + + -- analog outputs -- + + -- facility + F_MATRIX_CHG = 29 -- analog charge level of the induction matrix } rsio.IO_LVL = IO_LVL rsio.IO_DIR = IO_DIR rsio.IO_MODE = IO_MODE rsio.IO = IO_PORT -rsio.NUM_PORTS = IO_PORT.U_EMER_COOL + +rsio.NUM_PORTS = 29 +rsio.NUM_DIG_PORTS = 28 +rsio.NUM_ANA_PORTS = 1 -- self checks +assert(rsio.NUM_PORTS == (rsio.NUM_DIG_PORTS + rsio.NUM_ANA_PORTS), "port counts inconsistent") + local dup_chk = {} for _, v in pairs(IO_PORT) do assert(dup_chk[v] ~= true, "duplicate in port list") @@ -96,64 +108,45 @@ assert(#dup_chk == rsio.NUM_PORTS, "port list malformed") --#endregion ---#region Utility Functions +--#region Utility Functions and Attribute Tables -local PORT_NAMES = { - "F_SCRAM", - "F_ACK", - "R_SCRAM", - "R_RESET", - "R_ENABLE", - "U_ACK", - "F_ALARM", - "F_ALARM_ANY", - "WASTE_PU", - "WASTE_PO", - "WASTE_POPL", - "WASTE_AM", - "R_ACTIVE", - "R_AUTO_CTRL", - "R_SCRAMMED", - "R_AUTO_SCRAM", - "R_HIGH_DMG", - "R_HIGH_TEMP", - "R_LOW_COOLANT", - "R_EXCESS_HC", - "R_EXCESS_WS", - "R_INSUFF_FUEL", - "R_PLC_FAULT", - "R_PLC_TIMEOUT", - "U_ALARM", - "U_EMER_COOL" -} +local IO = IO_PORT +-- list of all port names +local PORT_NAMES = {} +for k, v in pairs(IO) do PORT_NAMES[v] = k end + +-- list of all port I/O modes local MODES = { - IO_MODE.DIGITAL_IN, -- F_SCRAM - IO_MODE.DIGITAL_IN, -- F_ACK - IO_MODE.DIGITAL_IN, -- R_SCRAM - IO_MODE.DIGITAL_IN, -- R_RESET - IO_MODE.DIGITAL_IN, -- R_ENABLE - IO_MODE.DIGITAL_IN, -- U_ACK - IO_MODE.DIGITAL_OUT, -- F_ALARM - IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY - IO_MODE.DIGITAL_OUT, -- WASTE_PU - IO_MODE.DIGITAL_OUT, -- WASTE_PO - IO_MODE.DIGITAL_OUT, -- WASTE_POPL - IO_MODE.DIGITAL_OUT, -- WASTE_AM - IO_MODE.DIGITAL_OUT, -- R_ACTIVE - IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL - IO_MODE.DIGITAL_OUT, -- R_SCRAMMED - IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM - IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG - IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP - IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT - IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC - IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS - IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL - IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT - IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT - IO_MODE.DIGITAL_OUT, -- U_ALARM - IO_MODE.DIGITAL_OUT -- U_EMER_COOL + [IO.F_SCRAM] = IO_MODE.DIGITAL_IN, + [IO.F_ACK] = IO_MODE.DIGITAL_IN, + [IO.R_SCRAM] = IO_MODE.DIGITAL_IN, + [IO.R_RESET] = IO_MODE.DIGITAL_IN, + [IO.R_ENABLE] = IO_MODE.DIGITAL_IN, + [IO.U_ACK] = IO_MODE.DIGITAL_IN, + [IO.F_ALARM] = IO_MODE.DIGITAL_OUT, + [IO.F_ALARM_ANY] = IO_MODE.DIGITAL_OUT, + [IO.F_MATRIX_LOW] = IO_MODE.DIGITAL_OUT, + [IO.F_MATRIX_HIGH] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_PU] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_PO] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_POPL] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_AM] = IO_MODE.DIGITAL_OUT, + [IO.R_ACTIVE] = IO_MODE.DIGITAL_OUT, + [IO.R_AUTO_CTRL] = IO_MODE.DIGITAL_OUT, + [IO.R_SCRAMMED] = IO_MODE.DIGITAL_OUT, + [IO.R_AUTO_SCRAM] = IO_MODE.DIGITAL_OUT, + [IO.R_HIGH_DMG] = IO_MODE.DIGITAL_OUT, + [IO.R_HIGH_TEMP] = IO_MODE.DIGITAL_OUT, + [IO.R_LOW_COOLANT] = IO_MODE.DIGITAL_OUT, + [IO.R_EXCESS_HC] = IO_MODE.DIGITAL_OUT, + [IO.R_EXCESS_WS] = IO_MODE.DIGITAL_OUT, + [IO.R_INSUFF_FUEL] = IO_MODE.DIGITAL_OUT, + [IO.R_PLC_FAULT] = IO_MODE.DIGITAL_OUT, + [IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT, + [IO.U_ALARM] = IO_MODE.DIGITAL_OUT, + [IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT, + [IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT } assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect") @@ -179,74 +172,51 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur -- I/O mappings to I/O function and I/O mode local RS_DIO_MAP = { - -- F_SCRAM - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, - -- F_ACK - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.F_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, + [IO.F_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- R_SCRAM - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, - -- R_RESET - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- R_ENABLE - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.R_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, + [IO.R_RESET] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.R_ENABLE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- U_ACK - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- F_ALARM - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- F_ALARM_ANY - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_ALARM_ANY] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_MATRIX_LOW] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_MATRIX_HIGH] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- WASTE_PU - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- WASTE_PO - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- WASTE_POPL - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- WASTE_AM - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_PU] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_PO] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_POPL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_AM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- R_ACTIVE - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_AUTO_CTRL - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_SCRAMMED - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_AUTO_SCRAM - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_HIGH_DMG - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_HIGH_TEMP - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_LOW_COOLANT - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_EXCESS_HC - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_EXCESS_WS - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_INSUFF_FUEL - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_PLC_FAULT - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_PLC_TIMEOUT - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_ACTIVE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_AUTO_CTRL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_SCRAMMED] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_AUTO_SCRAM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_HIGH_DMG] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_HIGH_TEMP] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_LOW_COOLANT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_EXCESS_HC] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_EXCESS_WS] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_INSUFF_FUEL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_PLC_FAULT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- U_ALARM - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- U_EMER_COOL - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT } + [IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT } } -assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect") +assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect") -- get the I/O direction of a port ---@nodiscard ---@param port IO_PORT ---@return IO_DIR function rsio.get_io_dir(port) - if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode + if rsio.is_valid_port(port) then + return util.trinary(MODES[port] == IO_MODE.DIGITAL_OUT or MODES[port] == IO_MODE.ANALOG_OUT, IO_DIR.OUT, IO_DIR.IN) else return IO_DIR.IN end end @@ -310,6 +280,13 @@ end --#region Digital I/O +-- check if a port is digital +---@nodiscard +---@param port IO_PORT +function rsio.is_digital(port) + return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.DIGITAL_IN or MODES[port] == IO_MODE.DIGITAL_OUT) +end + -- get digital I/O level reading from a redstone boolean input value ---@nodiscard ---@param rs_value boolean raw value from redstone @@ -330,7 +307,7 @@ function rsio.digital_write(level) return level == IO_LVL.HIGH end ---@param active boolean state to convert to logic level ---@return IO_LVL|false function rsio.digital_write_active(port, active) - if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then + if not rsio.is_digital(port) then return false else return RS_DIO_MAP[port]._out(active) @@ -343,9 +320,7 @@ end ---@param level IO_LVL logic level ---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided function rsio.digital_is_active(port, level) - if not util.is_int(port) then - return nil - elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then + if (not rsio.is_digital(port)) or level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then return nil else return RS_DIO_MAP[port]._in(level) @@ -356,6 +331,13 @@ end --#region Analog I/O +-- check if a port is analog +---@nodiscard +---@param port IO_PORT +function rsio.is_analog(port) + return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.ANALOG_IN or MODES[port] == IO_MODE.ANALOG_OUT) +end + -- read an analog value scaled from min to max ---@nodiscard ---@param rs_value number redstone reading (0 to 15) @@ -372,7 +354,7 @@ end ---@param value number value to write (from min to max range) ---@param min number minimum of range ---@param max number maximum of range ----@return number rs_value scaled redstone reading (0 to 15) +---@return integer rs_value scaled redstone reading (0 to 15) function rsio.analog_write(value, min, max) local scaled_value = (value - min) / (max - min) return math.floor(scaled_value * 15) diff --git a/scada-common/util.lua b/scada-common/util.lua index 98a1667..2f92424 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.2.2" +util.version = "1.3.0" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 33cd267..d7058a8 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -300,8 +300,8 @@ function facility.new(config, cooling_conf) -- calculate moving averages for induction matrix if self.induction[1] ~= nil then - local matrix = self.induction[1] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local matrix = self.induction[1] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db charge_update = db.tanks.last_update rate_update = db.state.last_update @@ -774,6 +774,16 @@ function facility.new(config, cooling_conf) self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm) self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm) + + -- update induction matrix related outputs + if self.induction[1] ~= nil then + local matrix = self.induction[1] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db + + self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) + self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) + self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1) + end end --#endregion diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua index 1bdef5a..a369937 100644 --- a/supervisor/session/rsctl.lua +++ b/supervisor/session/rsctl.lua @@ -2,6 +2,8 @@ -- Redstone RTU Session I/O Controller -- +local rsio = require("scada-common.rsio") + local rsctl = {} -- create a new redstone RTU I/O controller @@ -16,7 +18,7 @@ function rsctl.new(redstone_rtus) ---@return boolean function public.is_connected(port) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local db = redstone_rtus[i].get_db() ---@type redstone_session_db if db.io[port] ~= nil then return true end end @@ -28,8 +30,8 @@ function rsctl.new(redstone_rtus) ---@param value boolean function public.digital_write(port, value) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil if io ~= nil then io.write(value) end end end @@ -40,12 +42,25 @@ function rsctl.new(redstone_rtus) ---@return boolean|nil function public.digital_read(port) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil if io ~= nil then return io.read() end end end + -- write to an analog redstone port (applies to all RTUs) + ---@param port IO_PORT + ---@param value number value + ---@param min number minimum value for scaling 0 to 15 + ---@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 db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_ana_io|nil + if io ~= nil then io.write(rsio.analog_write(value, min, max)) end + end + end + return public end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 1694ebe..2a2202c 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.6" +local SUPERVISOR_VERSION = "v1.3.7" local println = util.println local println_ts = util.println_ts diff --git a/test/rstest.lua b/test/rstest.lua index e322e28..ba0b156 100644 --- a/test/rstest.lua +++ b/test/rstest.lua @@ -1,16 +1,28 @@ require("/initenv").init_env() -local rsio = require("scada-common.rsio") -local util = require("scada-common.util") +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") local testutils = require("test.testutils") +local IO = rsio.IO +local IO_LVL = rsio.IO_LVL +local IO_MODE = rsio.IO_MODE + local print = util.print local println = util.println -local IO = rsio.IO -local IO_LVL = rsio.IO_LVL -local IO_MODE = rsio.IO_MODE +-- list of inverted digital signals
+-- just using the key for a quick lookup, value need to be not nil +local DIG_INV = { + [IO.F_SCRAM] = 0, + [IO.R_SCRAM] = 0, + [IO.WASTE_PU] = 0, + [IO.WASTE_PO] = 0, + [IO.WASTE_POPL] = 0, + [IO.WASTE_AM] = 0, + [IO.U_EMER_COOL] = 0 +} println("starting RSIO tester") println("") @@ -50,8 +62,8 @@ testutils.pause() println(">>> checking invalid ports:") -testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "") -testutils.test_func_nil("rsio.to_string", rsio.to_string, "") +testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "UNKNOWN") +testutils.test_func_nil("rsio.to_string", rsio.to_string, "UNKNOWN") testutils.test_func("rsio.get_io_mode", rsio.get_io_mode, { -1, 100, false }, IO_MODE.ANALOG_IN) testutils.test_func_nil("rsio.get_io_mode", rsio.get_io_mode, IO_MODE.ANALOG_IN) @@ -100,46 +112,35 @@ println(">>> checking port I/O:") print("rsio.digital_is_active(...): ") --- check input ports -assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.LOW) == true, "IO_F_SCRAM_HIGH") -assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.HIGH) == false, "IO_F_SCRAM_LOW") -assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.LOW) == true, "IO_R_SCRAM_HIGH") -assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.HIGH) == false, "IO_R_SCRAM_LOW") -assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.LOW) == false, "IO_R_ENABLE_HIGH") -assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.HIGH) == true, "IO_R_ENABLE_LOW") +-- check all digital ports +for i = 1, rsio.NUM_PORTS do + if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then + local high = DIG_INV[i] == nil + assert(rsio.digital_is_active(i, IO_LVL.LOW) == not high, "IO_" .. rsio.to_string(i) .. "_LOW") + assert(rsio.digital_is_active(i, IO_LVL.HIGH) == high, "IO_" .. rsio.to_string(i) .. "_HIGH") + end +end --- non-inputs should always return LOW -assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.LOW) == false, "IO_OUT_READ_LOW") -assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.HIGH) == false, "IO_OUT_READ_HIGH") +assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.LOW) == nil, "ANA_DIG_READ_LOW") +assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.HIGH) == nil, "ANA_DIG_READ_HIGH") println("PASS") --- check output ports +-- check digital write -print("rsio.digital_write(...): ") +print("rsio.digital_write_active(...): ") --- check output ports -assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.LOW, "IO_F_ALARM_LOW") -assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_HIGH") -assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.HIGH, "IO_WASTE_PU_HIGH") -assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_LOW") -assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.HIGH, "IO_WASTE_PO_HIGH") -assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_LOW") -assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.HIGH, "IO_WASTE_POPL_HIGH") -assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.LOW, "IO_WASTE_POPL_LOW") -assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.HIGH, "IO_WASTE_AM_HIGH") -assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_LOW") - --- check all reactor output ports (all are active high) -for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do - assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_PORT") - assert(rsio.digital_write_active(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW") - assert(rsio.digital_write_active(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH") +-- check all digital ports +for i = 1, rsio.NUM_PORTS do + if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then + local high = DIG_INV[i] == nil + assert(rsio.digital_write_active(i, not high) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW") + assert(rsio.digital_write_active(i, high) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH") + end end --- non-outputs should always return false -assert(rsio.digital_write_active(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_FALSE") -assert(rsio.digital_write_active(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_TRUE") +assert(rsio.digital_write_active(IO.F_MATRIX_CHG, true) == false, "ANA_DIG_WRITE_TRUE") +assert(rsio.digital_write_active(IO.F_MATRIX_CHG, false) == false, "ANA_DIG_WRITE_FALSE") println("PASS") From fb85c2f05bb342c8b6f5d85dabc7ac1775337751 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 21 Apr 2024 13:54:14 -0400 Subject: [PATCH 2/8] RTU configurator updates for redstone I/O clarity --- rtu/configure.lua | 74 +++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/rtu/configure.lua b/rtu/configure.lua index 5dc440a..efebdee 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -34,6 +34,8 @@ local tri = util.trinary local cpair = core.cpair local IO = rsio.IO +local IO_LVL = rsio.IO_LVL +local IO_MODE = rsio.IO_MODE local LEFT = core.ALIGN.LEFT local CENTER = core.ALIGN.CENTER @@ -446,7 +448,7 @@ local function config_view(display) TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"} local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} - local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end + local function censor_key(enable) censor(tri(enable, "*", nil)) end local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} @@ -559,7 +561,7 @@ local function config_view(display) PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} local function back_from_colors() - main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4)) + main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4)) tool_ctl.jumped_to_color = false recolor(1) end @@ -901,7 +903,7 @@ local function config_view(display) tool_ctl.p_desc.reposition(1, 8) tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.") elseif type == "inductionPort" or type == "spsPort" then - local dev = util.trinary(type == "inductionPort", "induction matrix", "SPS") + local dev = tri(type == "inductionPort", "induction matrix", "SPS") tool_ctl.p_idx.hide(true) tool_ctl.p_unit.hide(true) tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.") @@ -927,7 +929,7 @@ local function config_view(display) tool_ctl.ppm_devs.remove_all() for name, entry in pairs(mounts) do if util.table_contains(RTU_DEV_TYPES, entry.type) then - local bkg = util.trinary(alternate, colors.white, colors.lightGray) + local bkg = tri(alternate, colors.white, colors.lightGray) ---@cast entry ppm_entry local line = Div{parent=tool_ctl.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)} @@ -1089,8 +1091,9 @@ local function config_view(display) 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_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}} + 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}} TextBox{parent=rs_cfg,x=1,y=2,height=1,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} @@ -1147,9 +1150,23 @@ local function config_view(display) text = "You selected the ALL_WASTE shortcut." else tool_ctl.rs_cfg_shortcut.hide(true) - tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) + tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) tool_ctl.rs_cfg_color.show() - text = "You selected " .. rsio.to_string(port) .. " (for " + + local io_type = "analog input " + local io_mode = rsio.get_io_mode(port) + local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "") + + if io_mode == IO_MODE.DIGITAL_IN then + io_type = inv .. "digital input " + elseif io_mode == IO_MODE.DIGITAL_OUT then + io_type = inv .. "digital output " + elseif io_mode == IO_MODE.ANALOG_OUT then + io_type = "analog output " + end + + text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for " + if PORT_DSGN[port] == 1 then text = text .. "a unit)." tool_ctl.rs_cfg_unit_l.show() @@ -1175,8 +1192,8 @@ local function config_view(display) for i = 1, rsio.NUM_PORTS do local p = PORT_DESC_MAP[i][1] local name = rsio.to_string(p) - local io_dir = util.trinary(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]") - local btn_color = util.trinary(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) + local io_dir = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]") + local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) local entry = Div{parent=rs_ports,height=1} PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} @@ -1186,13 +1203,20 @@ local function config_view(display) 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} - tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=1,text=""} + tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""} - tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=27,y=3,width=7,height=1,text="Unit ID"} - tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=27,y=4,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} + 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} - tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=3,width=11,height=1,text="Output Side"} - local side = Radio2D{parent=rs_c_3,x=1,y=4,rows=2,columns=3,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red} + 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} + + tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,height=1,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} + + tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,height=1,text="Unit ID"} + tool_ctl.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} local function set_bundled(bundled) if bundled then tool_ctl.rs_cfg_color.enable() else tool_ctl.rs_cfg_color.disable() end @@ -1223,10 +1247,10 @@ local function config_view(display) if port >= 0 then ---@type rtu_rs_definition local def = { - unit = util.trinary(PORT_DSGN[port] == 1, u, nil), + unit = tri(PORT_DSGN[port] == 1, u, nil), port = port, side = side_options_map[side.get_value()], - color = util.trinary(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil) + color = tri(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil) } if tool_ctl.rs_cfg_editing == false then @@ -1240,10 +1264,10 @@ local function config_view(display) local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime } for i = 0, 3 do table.insert(tmp_cfg.Redstone, { - unit = util.trinary(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), + unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil), port = IO.WASTE_PU + i, - side = util.trinary(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), - color = util.trinary(bundled.get_value(), default_colors[i + 1], nil) + side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), + color = tri(bundled.get_value(), default_colors[i + 1], nil) }) end end @@ -1296,7 +1320,7 @@ local function config_view(display) peri_import_list.remove_all() for _, entry in ipairs(config.RTU_DEVICES) do local for_facility = entry.for_reactor == 0 - local ini_unit = util.trinary(for_facility, nil, entry.for_reactor) + local ini_unit = tri(for_facility, nil, entry.for_reactor) local def = { name = entry.name, unit = ini_unit, index = entry.index } local mount = mounts[def.name] ---@type ppm_entry|nil @@ -1375,7 +1399,7 @@ local function config_view(display) table.insert(tmp_cfg.Redstone, def) local name = rsio.to_string(def.port) - local io_dir = util.trinary(rsio.get_io_dir(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 conn = def.side local unit = "facility" @@ -1438,7 +1462,7 @@ local function config_view(display) local val = util.strval(raw) 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] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace") elseif f[1] == "FrontPanelTheme" then val = util.strval(themes.fp_theme_name(raw)) elseif f[1] == "ColorMode" then @@ -1447,7 +1471,7 @@ local function config_view(display) if val == "nil" then val = "" end - local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) + local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) alternate = not alternate if string.len(val) > val_max_w then @@ -1561,7 +1585,7 @@ local function config_view(display) end tool_ctl.rs_cfg_selection.set_value(text) - tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) + tool_ctl.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)) bundled.set_value(def.color ~= nil) tool_ctl.rs_cfg_color.set_value(value) @@ -1582,7 +1606,7 @@ local function config_view(display) local def = cfg.Redstone[i] ---@type rtu_rs_definition local name = rsio.to_string(def.port) - local io_dir = util.trinary(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") + local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") local conn = def.side local unit = util.strval(def.unit or "F") From 35bf56663f45e54b393e00a439a7eba030222273 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 27 Apr 2024 16:27:01 -0400 Subject: [PATCH 3/8] #469 induction matrix charge ETAs and misc cleanup/updates --- coordinator/iocontrol.lua | 24 +++++-- coordinator/startup.lua | 2 +- coordinator/ui/components/imatrix.lua | 98 ++++++++++++++++++++------- scada-common/comms.lua | 2 +- scada-common/util.lua | 11 ++- supervisor/facility.lua | 51 ++++++++++---- supervisor/startup.lua | 2 +- supervisor/unit.lua | 4 +- 8 files changed, 142 insertions(+), 52 deletions(-) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 1de337f..7fc2707 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -663,10 +663,26 @@ function iocontrol.update_facility_status(status) fac.rtu_count = rtu_statuses.count -- power statistics - if type(rtu_statuses.power) == "table" then - fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1]) - fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2]) - fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3]) + if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then + local data = fac.induction_data_tbl[1] ---@type imatrix_session_db + local ps = fac.induction_ps_tbl[1] ---@type psil + + local chg = tonumber(rtu_statuses.power[1]) + local in_f = tonumber(rtu_statuses.power[2]) + local out_f = tonumber(rtu_statuses.power[3]) + local eta = tonumber(rtu_statuses.power[4]) + + ps.publish("avg_charge", chg) + ps.publish("avg_inflow", in_f) + ps.publish("avg_outflow", out_f) + ps.publish("eta_ms", eta) + + ps.publish("is_charging", in_f > out_f) + ps.publish("is_discharging", out_f > in_f) + + if data and data.build then + ps.publish("at_max_io", in_f >= data.build.transfer_cap or out_f >= data.build.transfer_cap) + end else log.debug(log_header .. "power statistics list not a table") valid = false diff --git a/coordinator/startup.lua b/coordinator/startup.lua index a50da5b..b3df178 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.4.3" +local COORDINATOR_VERSION = "v1.4.4" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index 2b80350..60a745a 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -9,6 +9,7 @@ local Rectangle = require("graphics.elements.rectangle") local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") +local IndicatorLight = require("graphics.elements.indicators.light") local PowerIndicator = require("graphics.elements.indicators.power") local StateIndicator = require("graphics.elements.indicators.state") local VerticalBar = require("graphics.elements.indicators.vbar") @@ -26,9 +27,13 @@ local ALIGN = core.ALIGN ---@param ps psil ps interface ---@param id number? matrix ID local function new_view(root, x, y, data, ps, id) + local label_fg = style.theme.label_fg local text_fg = style.theme.text_fg local lu_col = style.lu_colors + local ind_yel = style.ind_yel + local ind_wht = style.ind_wht + local title = "INDUCTION MATRIX" if type(id) == "number" then title = title .. id end @@ -42,45 +47,47 @@ local function new_view(root, x, y, data, ps, id) local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} - local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} - local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg} - local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg} - local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} - local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} - - local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg} - local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} - local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} + local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg} + local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg} + local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg} + local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} status.register(ps, "computed_status", status.update) - energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end) capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end) - input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end) - output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end) - + energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end) avg_chg.register(ps, "avg_charge", avg_chg.update) + input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end) avg_in.register(ps, "avg_inflow", avg_in.update) + output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end) avg_out.register(ps, "avg_outflow", avg_out.update) + trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end) - local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg} - - local cells = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg} - local providers = DataIndicator{parent=rect,x=11,y=15,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg} - - TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=style.theme.label_fg} - local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg} + local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg} + local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg} + local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg} + fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end) cells.register(ps, "cells", cells.update) providers.register(ps, "providers", providers.update) - fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end) - trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end) + + local chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht} + local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht} + local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel} + + chging.register(ps, "is_charging", chging.update) + dischg.register(ps, "is_discharging", dischg.update) + max_io.register(ps, "at_max_io", max_io.update) local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4} local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1} local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1} - TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg} - TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg} + TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg} local function calc_saturation(val) if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then @@ -91,6 +98,49 @@ local function new_view(root, x, y, data, ps, id) charge.register(ps, "energy_fill", charge.update) in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end) out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end) + + local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box} + + eta.register(ps, "eta_mss", function (eta_ms) + local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ") + + local seconds = math.abs(eta_ms) / 1000 + local minutes = seconds / 60 + local hours = minutes / 60 + local days = hours / 24 + + if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then + -- really small or NaN + str = "No ETA" + elseif days < 1000 then + days = math.floor(days) + hours = math.floor(hours % 24) + minutes = math.floor(minutes % 60) + seconds = math.floor(seconds % 60) + + if days > 0 then + str = days .. "d" + elseif hours > 0 then + str = hours .. "h " .. minutes .. "m" + elseif minutes > 0 then + str = minutes .. "m " .. seconds .. "s" + elseif seconds > 0 then + str = seconds .. "s" + end + + str = pre .. str + else + local years = math.floor(days / 365.25) + + if years <= 99999999 then + str = pre .. years .. "y" + else + str = pre .. "eras" + end + end + + eta.set_value(str) + end) end return new_view diff --git a/scada-common/comms.lua b/scada-common/comms.lua index fffaaee..cce000c 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 = "2.5.0" +comms.version = "2.5.1" comms.api_version = "0.0.1" ---@enum PROTOCOL diff --git a/scada-common/util.lua b/scada-common/util.lua index 2f92424..d5c85fa 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.3.0" +util.version = "1.3.1" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -181,8 +181,7 @@ function util.round(x) return math.floor(x + 0.5) end -- get a new moving average object ---@nodiscard ---@param length integer history length ----@param default number value to fill history with for first call to compute() -function util.mov_avg(length, default) +function util.mov_avg(length) local data = {} local index = 1 local last_t = 0 ---@type number|nil @@ -215,12 +214,10 @@ function util.mov_avg(length, default) ---@return number average function public.compute() local sum = 0 - for i = 1, length do sum = sum + data[i] end - return sum / length + for i = 1, #data do sum = sum + data[i] end + return sum / #data end - public.reset(default) - return public end diff --git a/supervisor/facility.lua b/supervisor/facility.lua index d7058a8..0fd0ed9 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -128,9 +128,13 @@ function facility.new(config, cooling_conf) test_alarm_states = {}, -- statistics im_stat_init = false, - avg_charge = util.mov_avg(3, 0.0), - avg_inflow = util.mov_avg(6, 0.0), - avg_outflow = util.mov_avg(6, 0.0) + avg_charge = util.mov_avg(3), -- 3 seconds + avg_inflow = util.mov_avg(6), -- 3 seconds + avg_outflow = util.mov_avg(6), -- 3 seconds + -- induction matrix charge delta stats + avg_net = util.mov_avg(60), -- 60 seconds + charge_last = 0, + charge_last_t = 0 } -- create units @@ -307,15 +311,32 @@ function facility.new(config, cooling_conf) rate_update = db.state.last_update if (charge_update > 0) and (rate_update > 0) then + local energy = util.joules_to_fe(db.tanks.energy) + local input = util.joules_to_fe(db.state.last_input) + local output = util.joules_to_fe(db.state.last_output) + if self.im_stat_init then - self.avg_charge.record(util.joules_to_fe(db.tanks.energy), charge_update) - self.avg_inflow.record(util.joules_to_fe(db.state.last_input), rate_update) - self.avg_outflow.record(util.joules_to_fe(db.state.last_output), rate_update) + self.avg_charge.record(energy, charge_update) + self.avg_inflow.record(input, rate_update) + self.avg_outflow.record(output, rate_update) + + if charge_update ~= self.charge_last_t then + local delta = (energy - self.charge_last) / (charge_update - self.charge_last_t) + + self.charge_last = energy + self.charge_last_t = charge_update + + self.avg_net.record(delta, charge_update) + end else self.im_stat_init = true - self.avg_charge.reset(util.joules_to_fe(db.tanks.energy)) - self.avg_inflow.reset(util.joules_to_fe(db.state.last_input)) - self.avg_outflow.reset(util.joules_to_fe(db.state.last_output)) + + self.avg_charge.reset(energy) + self.avg_inflow.reset(input) + self.avg_outflow.reset(output) + + self.charge_last = energy + self.charge_last_t = charge_update end end else @@ -1193,15 +1214,21 @@ function facility.new(config, cooling_conf) status.power = { self.avg_charge.compute(), self.avg_inflow.compute(), - self.avg_outflow.compute() + self.avg_outflow.compute(), + 0 } -- status of induction matricies (including tanks) status.induction = {} for i = 1, #self.induction do - local matrix = self.induction[i] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local matrix = self.induction[i] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db + status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks } + + local fe_per_ms = self.avg_net.compute() + local remaining = util.joules_to_fe(util.trinary(fe_per_ms >= 0, db.tanks.energy_need, db.tanks.energy)) + status.power[4] = remaining / fe_per_ms end -- status of sps diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 2a2202c..d15ddc5 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.7" +local SUPERVISOR_VERSION = "v1.3.8" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 6fa4d0a..f0dccf2 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -71,8 +71,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@class _unit_self local self = { r_id = reactor_id, - plc_s = nil, ---@class plc_session_struct - plc_i = nil, ---@class plc_session + plc_s = nil, ---@type plc_session_struct + plc_i = nil, ---@type plc_session num_boilers = num_boilers, num_turbines = num_turbines, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, From 826086951e211fdefbcc3159fb91dea03b491b84 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 27 Apr 2024 19:50:35 -0400 Subject: [PATCH 4/8] return zero on mov_avg compute if no samples --- scada-common/util.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index d5c85fa..3b876e3 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -213,8 +213,13 @@ function util.mov_avg(length) ---@nodiscard ---@return number average function public.compute() + if #data == 0 then return 0 end + local sum = 0 - for i = 1, #data do sum = sum + data[i] end + for i = 1, #data do + sum = sum + data[i] + end + return sum / #data end From 6f768ef6b3aea88defdd8c1944e7c419a8c0d5d0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 28 Apr 2024 01:26:44 -0400 Subject: [PATCH 5/8] #469 made ETA tolerant to induction matrix capacity changes --- scada-common/util.lua | 10 +++++++--- supervisor/facility.lua | 15 ++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index 3b876e3..d29a85e 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -189,11 +189,15 @@ function util.mov_avg(length) ---@class moving_average local public = {} - -- reset all to a given value - ---@param x number value + -- reset all to a given value, or clear all data if no value is given + ---@param x number? value function public.reset(x) + index = 1 data = {} - for _ = 1, length do t_insert(data, x) end + + if x then + for _ = 1, length do t_insert(data, x) end + end end -- record a new value diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 0fd0ed9..51283d1 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -133,6 +133,7 @@ function facility.new(config, cooling_conf) avg_outflow = util.mov_avg(6), -- 3 seconds -- induction matrix charge delta stats avg_net = util.mov_avg(60), -- 60 seconds + last_capacity = 0, charge_last = 0, charge_last_t = 0 } @@ -326,7 +327,13 @@ function facility.new(config, cooling_conf) self.charge_last = energy self.charge_last_t = charge_update - self.avg_net.record(delta, charge_update) + -- if the capacity changed, toss out existing data + if db.build.max_energy ~= self.last_capacity then + self.last_capacity = db.build.max_energy + self.avg_net.reset() + else + self.avg_net.record(delta, charge_update) + end end else self.im_stat_init = true @@ -641,8 +648,7 @@ function facility.new(config, cooling_conf) local astatus = self.ascram_status if self.induction[1] ~= nil then - local matrix = self.induction[1] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local db = self.induction[1].get_db() ---@type imatrix_session_db -- clear matrix disconnected if astatus.matrix_dc then @@ -798,8 +804,7 @@ function facility.new(config, cooling_conf) -- update induction matrix related outputs if self.induction[1] ~= nil then - local matrix = self.induction[1] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local db = self.induction[1].get_db() ---@type imatrix_session_db self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) From 50bf057ca61584d7f2fb68aeaeb6cf91343d1e37 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 28 Apr 2024 02:01:21 -0400 Subject: [PATCH 6/8] #412 optionally disable SPS at low power --- coordinator/iocontrol.lua | 5 +++- coordinator/process.lua | 18 +++++++++++- coordinator/ui/components/process_ctl.lua | 34 +++++++++-------------- scada-common/comms.lua | 3 +- supervisor/facility.lua | 33 ++++++++++++++++++++-- supervisor/session/coordinator.lua | 6 ++++ 6 files changed, 72 insertions(+), 27 deletions(-) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 7fc2707..5edd724 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -92,6 +92,7 @@ function iocontrol.init(conf, comms, temp_scale) ---@type WASTE_PRODUCT auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, auto_pu_fallback_active = false, + auto_sps_disabled = false, radiation = types.new_zero_radiation_reading(), @@ -593,7 +594,7 @@ function iocontrol.update_facility_status(status) local ctl_status = status[1] - if type(ctl_status) == "table" and #ctl_status == 16 then + if type(ctl_status) == "table" and #ctl_status == 17 then fac.all_sys_ok = ctl_status[1] fac.auto_ready = ctl_status[2] @@ -644,9 +645,11 @@ function iocontrol.update_facility_status(status) fac.auto_current_waste_product = ctl_status[15] fac.auto_pu_fallback_active = ctl_status[16] + fac.auto_sps_disabled = ctl_status[17] fac.ps.publish("current_waste_product", fac.auto_current_waste_product) fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active) + fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled) else log.debug(log_header .. "control status not a table or length mismatch") valid = false diff --git a/coordinator/process.lua b/coordinator/process.lua index 581fcd9..1016e62 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -29,7 +29,8 @@ local self = { gen_target = 0.0, limits = {}, waste_product = PRODUCT.PLUTONIUM, - pu_fallback = false + pu_fallback = false, + sps_low_power = false }, waste_modes = {}, priority_groups = {} @@ -65,6 +66,7 @@ function process.init(iocontrol, coord_comms) ctl_proc.limits = config.limits ctl_proc.waste_product = config.waste_product ctl_proc.pu_fallback = config.pu_fallback + ctl_proc.sps_low_power = config.sps_low_power self.io.facility.ps.publish("process_mode", ctl_proc.mode) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) @@ -72,6 +74,7 @@ function process.init(iocontrol, coord_comms) self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product) self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback) + self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power) for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do local unit = self.io.units[id] ---@type ioctl_unit @@ -83,6 +86,7 @@ function process.init(iocontrol, coord_comms) -- notify supervisor of auto waste config self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product) self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback) + self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power) end -- unit waste states @@ -259,6 +263,18 @@ function process.set_pu_fallback(enabled) _write_auto_config() end +-- set automatic process control SPS usage at low power +---@param enabled boolean whether to enable SPS usage at low power +function process.set_sps_low_power(enabled) + self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled) + + log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled)) + + -- update config table and save + self.control_states.process.sps_low_power = enabled + _write_auto_config() +end + -- save process control settings ---@param mode PROCESS control mode ---@param burn_target number burn rate target diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index 430409b..92ed8bd 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -341,31 +341,23 @@ local function new_view(root, x, y) 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 pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,style.theme.checkbox_bg)} - waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) - pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value) + local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht} + local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel} - local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht} + local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)} + + TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label} + + local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)} + + TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label} fb_active.register(facility.ps, "pu_fallback_active", fb_active.update) - - TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label} - local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17} - - TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label} - local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17} - - TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label} - local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17} - - pu_rate.register(facility.ps, "pu_rate", pu_rate.update) - po_rate.register(facility.ps, "po_rate", po_rate.update) - am_rate.register(facility.ps, "am_rate", am_rate.update) - - local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17} - - sna_count.register(facility.ps, "sna_count", sna_count.update) + sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update) + waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) + pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value) + lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value) end return new_view diff --git a/scada-common/comms.lua b/scada-common/comms.lua index cce000c..e107d33 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -97,7 +97,8 @@ local FAC_COMMAND = { START = 2, -- start automatic process control ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units SET_WASTE_MODE = 4, -- set automatic waste processing mode - SET_PU_FB = 5 -- set plutonium fallback mode + SET_PU_FB = 5, -- set plutonium fallback mode + SET_SPS_LP = 6 -- set SPS at low power mode } ---@enum UNIT_COMMAND diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 51283d1..94d6af1 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -120,6 +120,8 @@ function facility.new(config, cooling_conf) waste_product = WASTE.PLUTONIUM, current_waste_product = WASTE.PLUTONIUM, pu_fallback = false, + sps_low_power = false, + disabled_sps = false, -- alarm tones tone_states = {}, test_tone_set = false, @@ -840,9 +842,25 @@ function facility.new(config, cooling_conf) end -- update waste product - if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then + + self.current_waste_product = self.waste_product + + if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then + local db = self.induction[1].get_db() ---@type imatrix_session_db + + if db.tanks.energy_fill >= 0.15 then + self.disabled_sps = false + elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then + self.disabled_sps = true + self.current_waste_product = WASTE.POLONIUM + end + else + self.disabled_sps = false + end + + if self.pu_fallback and insufficent_po_rate then self.current_waste_product = WASTE.PLUTONIUM - else self.current_waste_product = self.waste_product end + end -- make sure dynamic tanks are allowing outflow if required -- set all, rather than trying to determine which is for which (simpler & safer) @@ -1099,6 +1117,14 @@ function facility.new(config, cooling_conf) return self.pu_fallback end + -- enable/disable SPS at low power + ---@param enabled boolean requested state + ---@return boolean enabled newly set value + function public.set_sps_low_power(enabled) + self.sps_low_power = enabled == true + return self.sps_low_power + end + --#endregion --#region Diagnostic Testing @@ -1203,7 +1229,8 @@ function facility.new(config, cooling_conf) self.status_text[2], self.group_map, self.current_waste_product, - (self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM) + self.pu_fallback and (self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM), + self.disabled_sps } end diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index c0e6482..9c9284f 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -270,6 +270,12 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil else log.debug(log_header .. "CRDN set pu fallback packet length mismatch") end + elseif cmd == FAC_COMMAND.SET_SPS_LP then + if pkt.length == 2 then + _send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) }) + else + log.debug(log_header .. "CRDN set sps low power packet length mismatch") + end else log.debug(log_header .. "CRDN facility command unknown") end From 165d1497f86544728ed252378c9e9e67e70cef93 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 28 Apr 2024 02:01:40 -0400 Subject: [PATCH 7/8] reverted test change that got committed --- coordinator/ui/components/imatrix.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index 60a745a..b116e3d 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -101,7 +101,7 @@ local function new_view(root, x, y, data, ps, id) local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box} - eta.register(ps, "eta_mss", function (eta_ms) + eta.register(ps, "eta_ms", function (eta_ms) local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ") local seconds = math.abs(eta_ms) / 1000 From d35b824458d3fbd2b78f55c05c0a321e6210aad0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 28 Apr 2024 13:08:16 -0400 Subject: [PATCH 8/8] luacheck fix and cleanup --- coordinator/ui/components/process_ctl.lua | 8 +++++--- scada-common/rsio.lua | 2 +- scada-common/util.lua | 2 +- supervisor/facility.lua | 22 +++++++++++----------- supervisor/startup.lua | 2 +- test/rstest.lua | 2 +- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index 92ed8bd..6435de7 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -342,9 +342,14 @@ local function new_view(root, x, y) 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} + waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) + local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht} local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel} + fb_active.register(facility.ps, "pu_fallback_active", fb_active.update) + sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update) + local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)} TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label} @@ -353,9 +358,6 @@ local function new_view(root, x, y) TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label} - fb_active.register(facility.ps, "pu_fallback_active", fb_active.update) - sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update) - waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value) lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value) end diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 8f4c558..fb3a50a 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -52,7 +52,7 @@ local IO_PORT = { -- facility F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm) F_ALARM_ANY = 8, -- active high, any alarm regardless of priority - F_MATRIX_LOW = 27, -- active high, induction matrix charge less than + F_MATRIX_LOW = 27, -- active high, induction matrix charge low F_MATRIX_HIGH = 28, -- active high, induction matrix charge high -- waste diff --git a/scada-common/util.lua b/scada-common/util.lua index d29a85e..0fe636d 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.3.1" +util.version = "1.3.0" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 94d6af1..937a8e7 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -135,9 +135,9 @@ function facility.new(config, cooling_conf) avg_outflow = util.mov_avg(6), -- 3 seconds -- induction matrix charge delta stats avg_net = util.mov_avg(60), -- 60 seconds - last_capacity = 0, - charge_last = 0, - charge_last_t = 0 + imtx_last_capacity = 0, + imtx_last_charge = 0, + imtx_last_charge_t = 0 } -- create units @@ -323,15 +323,15 @@ function facility.new(config, cooling_conf) self.avg_inflow.record(input, rate_update) self.avg_outflow.record(output, rate_update) - if charge_update ~= self.charge_last_t then - local delta = (energy - self.charge_last) / (charge_update - self.charge_last_t) + if charge_update ~= self.imtx_last_charge_t then + local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t) - self.charge_last = energy - self.charge_last_t = charge_update + self.imtx_last_charge = energy + self.imtx_last_charge_t = charge_update -- if the capacity changed, toss out existing data - if db.build.max_energy ~= self.last_capacity then - self.last_capacity = db.build.max_energy + if db.build.max_energy ~= self.imtx_last_capacity then + self.imtx_last_capacity = db.build.max_energy self.avg_net.reset() else self.avg_net.record(delta, charge_update) @@ -344,8 +344,8 @@ function facility.new(config, cooling_conf) self.avg_inflow.reset(input) self.avg_outflow.reset(output) - self.charge_last = energy - self.charge_last_t = charge_update + self.imtx_last_charge = energy + self.imtx_last_charge_t = charge_update end end else diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d15ddc5..2a2202c 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.8" +local SUPERVISOR_VERSION = "v1.3.7" local println = util.println local println_ts = util.println_ts diff --git a/test/rstest.lua b/test/rstest.lua index ba0b156..612d0d8 100644 --- a/test/rstest.lua +++ b/test/rstest.lua @@ -13,7 +13,7 @@ local print = util.print local println = util.println -- list of inverted digital signals
--- just using the key for a quick lookup, value need to be not nil +-- only using the key for a quick lookup, value just can't be nil local DIG_INV = { [IO.F_SCRAM] = 0, [IO.R_SCRAM] = 0,