From a786404092ff3ccf5786470859345aa81dcf2b78 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 16 Apr 2024 15:55:40 -0400 Subject: [PATCH 01/15] #476 fixed PPM not initing fault counter in __index handler when a function is found --- scada-common/ppm.lua | 2 ++ scada-common/util.lua | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 34a6a609..e6a95e8d 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -141,7 +141,9 @@ local function peri_init(iface) local funcs = peripheral.wrap(iface) if (type(funcs) == "table") and (type(funcs[key]) == "function") then -- add this function then return it + self.fault_counts[key] = 0 self.device[key] = protect_peri_function(key, funcs[key]) + log.info(util.c("PPM: [@", iface, "] initialized previously undefined field ", key, "()")) return self.device[key] diff --git a/scada-common/util.lua b/scada-common/util.lua index 5433ab99..98a1667e 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.1" +util.version = "1.2.2" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 From d9efd5b8d2851f353916218dcad39c2e6fe5c855 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 20 Apr 2024 16:32:18 -0400 Subject: [PATCH 02/15] #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 | 228 ++++++++++++++++------------------- scada-common/util.lua | 2 +- supervisor/facility.lua | 14 ++- supervisor/session/rsctl.lua | 25 +++- supervisor/startup.lua | 2 +- test/rstest.lua | 83 ++++++------- 9 files changed, 235 insertions(+), 208 deletions(-) diff --git a/rtu/configure.lua b/rtu/configure.lua index 7758dc2f..5dc440a0 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 94701249..027549cd 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 20925bd3..678ea98f 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 6d1e6881..8f4c5580 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 - -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" -} +--#region Utility Functions and Attribute Tables + +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 }, - - -- 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 }, - - -- 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 }, - - -- 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 }, - - -- 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 }, - - -- 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.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 }, + + [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 }, + + [IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + + [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 }, + + [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 }, + + [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 }, + + [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 98a1667e..2f924245 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 33cd2673..d7058a84 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 1bdef5a6..a3699375 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 1694ebee..2a2202cf 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 e322e28f..ba0b1560 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 - -print("rsio.digital_write(...): ") - --- 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 digital write + +print("rsio.digital_write_active(...): ") + +-- 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 00a8d64a88ddd040197baf26f4e40fcae0df83cd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 20 Apr 2024 20:38:55 -0400 Subject: [PATCH 03/15] fixed coordinator not showing FAIL on unit count mismatch when connecting --- coordinator/coordinator.lua | 1 + coordinator/startup.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index e72cccfd..f6a20188 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -348,6 +348,7 @@ function coordinator.comms(version, nic, sv_watchdog) ok = false elseif self.sv_config_err then + self.est_task_done(false) coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs") ok = false elseif (os.clock() - self.est_last) > 1.0 then diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 0d89a87a..a50da5bd 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.2" +local COORDINATOR_VERSION = "v1.4.3" local CHUNK_LOAD_DELAY_S = 30.0 From fb85c2f05bb342c8b6f5d85dabc7ac1775337751 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 21 Apr 2024 13:54:14 -0400 Subject: [PATCH 04/15] 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 5dc440a0..efebdeec 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=""} + + 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} - 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} + 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_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} + 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 51d4a22532a4f77589f75b95a42b977cf0a700b1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 21 Apr 2024 13:55:39 -0400 Subject: [PATCH 05/15] #478 simplified reactor PLC reactor formed handling --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 65 +++++++++++++---------------------------- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 024daebc..18ea0914 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.7.9" +local R_PLC_VERSION = "v1.7.10" local println = util.println local println_ts = util.println_ts diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 22e1d86c..b3a02516 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -88,56 +88,32 @@ function threads.thread__main(smem, init) end end - -- are we now formed after waiting to be formed? + -- check for formed state change if (not plc_state.reactor_formed) and rps.is_formed() then - -- push a connect event and unmount it from the PPM - local iface = ppm.get_iface(plc_dev.reactor) - if iface then - log.info("unmounting and remounting unformed reactor") - ppm.unmount(plc_dev.reactor) + -- reactor now formed + plc_state.reactor_formed = true - local type, device = ppm.mount(iface) + println_ts("reactor is now formed.") + log.info("reactor is now formed") - if type == "fissionReactorLogicAdapter" and device ~= nil then - -- reconnect reactor - plc_dev.reactor = device + -- SCRAM newly formed reactor + smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) - -- we need to assume formed here as we cannot check in this main loop - -- RPS will identify if it isn't and this will get set false later - plc_state.reactor_formed = true - - println_ts("reactor reconnected.") - log.info("reactor reconnected") - - -- SCRAM newly connected reactor - smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) - - -- determine if we are still in a degraded state - if (not networked) or nic.is_connected() then - plc_state.degraded = false - end - - rps.reconnect_reactor(plc_dev.reactor) - if networked then - plc_comms.reconnect_reactor(plc_dev.reactor) - end - - -- partial reset of RPS, specific to becoming formed - rps.reset_formed() - else - -- fully lost the reactor now :( - println_ts("reactor lost (failed reconnect)!") - log.error("reactor lost (failed reconnect)") - - plc_state.no_reactor = true - plc_state.degraded = true - end - else - log.error("failed to get interface of previously connected reactor", true) + -- determine if we are still in a degraded state + if (not networked) or nic.is_connected() then + plc_state.degraded = false end - elseif not rps.is_formed() then + + -- partial reset of RPS, specific to becoming formed + -- without this, auto control can't resume on chunk load + rps.reset_formed() + elseif plc_state.reactor_formed and not rps.is_formed() then -- reactor no longer formed + println_ts("reactor is no longer formed.") + log.info("reactor is no longer formed") + plc_state.reactor_formed = false + plc_state.degraded = true end -- update indicators @@ -227,7 +203,8 @@ function threads.thread__main(smem, init) plc_comms.reconnect_reactor(plc_dev.reactor) end - -- partial reset of RPS, specific to becoming formed + -- partial reset of RPS, specific to becoming formed/reconnected + -- without this, auto control can't resume on chunk load rps.reset_formed() end elseif networked and type == "modem" then From 35bf56663f45e54b393e00a439a7eba030222273 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 27 Apr 2024 16:27:01 -0400 Subject: [PATCH 06/15] #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 1de337f7..7fc27075 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 a50da5bd..b3df1789 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 2b80350b..60a745a9 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 fffaaeed..cce000c3 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 2f924245..d5c85fa2 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 d7058a84..0fd0ed90 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 2a2202cf..d15ddc53 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 6fa4d0ae..f0dccf22 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 07/15] 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 d5c85fa2..3b876e3f 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 08/15] #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 3b876e3f..d29a85e7 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 0fd0ed90..51283d10 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 09/15] #412 optionally disable SPS at low power --- coordinator/iocontrol.lua | 5 +++- coordinator/process.lua | 18 ++++++++++++- coordinator/ui/components/process_ctl.lua | 30 ++++++++------------- scada-common/comms.lua | 3 ++- supervisor/facility.lua | 33 ++++++++++++++++++++--- supervisor/session/coordinator.lua | 6 +++++ 6 files changed, 70 insertions(+), 25 deletions(-) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 7fc27075..5edd724a 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 581fcd94..1016e62c 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 430409b8..92ed8bd6 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=9,label="Fallback Active",colors=ind_wht} - - fb_active.register(facility.ps, "pu_fallback_active", fb_active.update) + 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} - 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} + 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=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=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label} - 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} + 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)} - 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) + TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label} - 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) + 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 return new_view diff --git a/scada-common/comms.lua b/scada-common/comms.lua index cce000c3..e107d334 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 51283d10..94d6af12 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 c0e64825..9c9284fb 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 10/15] 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 60a745a9..b116e3da 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 11/15] 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 92ed8bd6..6435de77 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 8f4c5580..fb3a50a2 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 d29a85e7..0fe636d7 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 94d6af12..937a8e70 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 d15ddc53..2a2202cf 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 ba0b1560..612d0d80 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, From eb45ff899bdd3b70293e2803317ef1ef0c9fb27d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 29 Apr 2024 22:03:54 -0400 Subject: [PATCH 12/15] #455 calculate reactor temp high limit --- scada-common/constants.lua | 10 ++++++---- supervisor/session/plc.lua | 23 +++++++++++++++++++++-- supervisor/startup.lua | 2 +- supervisor/unit.lua | 3 ++- supervisor/unitlogic.lua | 19 ++++++++++++++----- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/scada-common/constants.lua b/scada-common/constants.lua index 678ea98f..b2755b95 100644 --- a/scada-common/constants.lua +++ b/scada-common/constants.lua @@ -29,7 +29,7 @@ local annunc = {} annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s annunc.RCSFlowLow_NA = -2.0 -- flow < -2.0 mB/s annunc.CoolantLevelLow = 0.4 -- fill < 40% -annunc.ReactorTempHigh = 1000 -- temp > 1000K +annunc.OpTempTolerance = 5 -- high temp if >= operational temp + X annunc.ReactorHighDeltaT = 50 -- rate > 50K/s annunc.FuelLevelLow = 0.05 -- fill <= 5% annunc.WasteLevelHigh = 0.80 -- fill >= 80% @@ -101,9 +101,11 @@ constants.EXTREME_RADIATION = 100.0 ---@class _mek_constants local mek = {} -mek.TURBINE_GAS_PER_TANK = 64000 -- mekanism: turbineGasPerTank -mek.TURBINE_DISPERSER_FLOW = 1280 -- mekanism: turbineDisperserGasFlow -mek.TURBINE_VENT_FLOW = 32000 -- mekanism: turbineVentGasFlow +mek.BASE_BOIL_TEMP = 373.15 -- mekanism: HeatUtils.BASE_BOIL_TEMP +mek.JOULES_PER_MB = 1000000 -- mekanism: energyPerFissionFuel +mek.TURBINE_GAS_PER_TANK = 64000 -- mekanism: turbineGasPerTank +mek.TURBINE_DISPERSER_FLOW = 1280 -- mekanism: turbineDisperserGasFlow +mek.TURBINE_VENT_FLOW = 32000 -- mekanism: turbineVentGasFlow constants.mek = mek diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 5bb097e9..e0588169 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -1,4 +1,5 @@ local comms = require("scada-common.comms") +local const = require("scada-common.constants") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local types = require("scada-common.types") @@ -105,6 +106,8 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f formed = false, rps_tripped = false, rps_trip_cause = "ok", ---@type rps_trip_cause + max_op_temp_H2O = 1200, + max_op_temp_Na = 1200, ---@class rps_status rps_status = { high_dmg = false, @@ -138,11 +141,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f waste = 0, waste_need = 0, waste_fill = 0.0, - ccool_type = "?", + ccool_type = types.FLUID.EMPTY_GAS, ---@type fluid ccool_amnt = 0, ccool_need = 0, ccool_fill = 0.0, - hcool_type = "?", + hcool_type = types.FLUID.EMPTY_GAS, ---@type fluid hcool_amnt = 0, hcool_need = 0, hcool_fill = 0.0 @@ -169,6 +172,21 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f ---@class plc_session local public = {} + -- compute maximum expected operational temperatures for high temp warnings + local function _compute_op_temps() + local JOULES_PER_MB = const.mek.JOULES_PER_MB + local BASE_BOIL_TEMP = const.mek.BASE_BOIL_TEMP + + local heat_cap = self.sDB.mek_struct.heat_cap + local max_burn = self.sDB.mek_struct.max_burn + + self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP + self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP + + log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3f (H2O) and %.3f (Na)", + self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na)) + end + -- copy in the RPS status ---@param rps_status table local function _copy_rps_status(rps_status) @@ -351,6 +369,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f local status = pcall(_copy_struct, pkt.data) if status then -- copied in structure data OK + _compute_op_temps() self.received_struct = true out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id) else diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 2a2202cf..d15ddc53 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 f0dccf22..8747b61c 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -147,7 +147,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) }, damage = 0, temp = 0, - waste = 0 + waste = 0, + high_temp_lim = 1150 }, ---@class alarm_monitors alarms = { diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index ad2b5221..3fe2ebcd 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -133,7 +133,15 @@ function logic.update_annunciator(self) self.last_heartbeat = plc_db.last_status_update end - local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O) + local flow_low = ANNUNC_LIMS.RCSFlowLow_H2O + local high_temp = plc_db.max_op_temp_H2O + + if plc_db.mek_status.ccool_type == types.FLUID.SODIUM then + flow_low = ANNUNC_LIMS.RCSFlowLow_NA + high_temp = plc_db.max_op_temp_Na + end + + self.plc_cache.high_temp_lim = math.min(high_temp + ANNUNC_LIMS.OpTempTolerance, 1200) -- update other annunciator fields annunc.ReactorSCRAM = plc_db.rps_tripped @@ -142,7 +150,7 @@ function logic.update_annunciator(self) annunc.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool) annunc.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low annunc.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow - annunc.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh + annunc.ReactorTempHigh = plc_db.mek_status.temp >= self.plc_cache.high_temp_lim annunc.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT annunc.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow annunc.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh @@ -542,7 +550,8 @@ function logic.update_alarms(self) end -- High Temperature - _update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp) + 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) -- Waste Leak _update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak) @@ -718,11 +727,11 @@ function logic.update_status_text(self) self.status_text[2] = "elevated level of radiation" end elseif is_active(self.alarms.ReactorOverTemp) then - self.status_text = { "CORE OVER TEMP", "reactor core temperature >=1200K" } + self.status_text = { "CORE OVER TEMP", "reactor core temp damaging" } elseif is_active(self.alarms.ReactorWasteLeak) then self.status_text = { "WASTE LEAK", "radioactive waste leak detected" } elseif is_active(self.alarms.ReactorHighTemp) then - self.status_text = { "CORE TEMP HIGH", "reactor core temperature >1150K" } + self.status_text = { "CORE TEMP HIGH", "reactor core temperature high" } elseif is_active(self.alarms.ReactorHighWaste) then self.status_text = { "WASTE LEVEL HIGH", "waste accumulating in reactor" } elseif is_active(self.alarms.TurbineTrip) then From f621ff2482fef3cdc134dfaaad850ff1c6b04a57 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 30 Apr 2024 18:54:01 -0400 Subject: [PATCH 13/15] added some value inits and unit labels --- supervisor/facility.lua | 2 ++ supervisor/session/plc.lua | 2 +- supervisor/startup.lua | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 937a8e70..efbc7d5c 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -343,7 +343,9 @@ function facility.new(config, cooling_conf) self.avg_charge.reset(energy) self.avg_inflow.reset(input) self.avg_outflow.reset(output) + self.avg_net.reset() + self.imtx_last_capacity = db.build.max_energy self.imtx_last_charge = energy self.imtx_last_charge_t = charge_update end diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index e0588169..a863fe27 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -183,7 +183,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP - log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3f (H2O) and %.3f (Na)", + log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)", self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na)) end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d15ddc53..0c78ff7a 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.9" local println = util.println local println_ts = util.println_ts From f958b0e3b787ed2289d88fb9778611223c2947b2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 30 Apr 2024 20:27:04 -0400 Subject: [PATCH 14/15] fixed at max i/o indicator --- coordinator/iocontrol.lua | 3 ++- coordinator/startup.lua | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 5edd724a..9aa3dbca 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -684,7 +684,8 @@ function iocontrol.update_facility_status(status) 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) + local cap = util.joules_to_fe(data.build.transfer_cap) + ps.publish("at_max_io", in_f >= cap or out_f >= cap) end else log.debug(log_header .. "power statistics list not a table") diff --git a/coordinator/startup.lua b/coordinator/startup.lua index b3df1789..f4de35d2 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.4" +local COORDINATOR_VERSION = "v1.4.5" local CHUNK_LOAD_DELAY_S = 30.0 From 25dc47d520d6dfb3c2f0de40c81f861f933a1a0d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 30 Apr 2024 20:28:07 -0400 Subject: [PATCH 15/15] fixed recording bad stats on induction matrix faults --- supervisor/facility.lua | 32 +++++++++++++++++++++++++++----- supervisor/startup.lua | 2 +- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/supervisor/facility.lua b/supervisor/facility.lua index efbc7d5c..8fb37463 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -137,7 +137,9 @@ function facility.new(config, cooling_conf) avg_net = util.mov_avg(60), -- 60 seconds imtx_last_capacity = 0, imtx_last_charge = 0, - imtx_last_charge_t = 0 + imtx_last_charge_t = 0, + -- track faulted induction matrix update times to reject + imtx_faulted_times = { 0, 0, 0 } } -- create units @@ -310,10 +312,26 @@ function facility.new(config, cooling_conf) local matrix = self.induction[1] ---@type unit_session local db = matrix.get_db() ---@type imatrix_session_db - charge_update = db.tanks.last_update + local build_update = db.build.last_update rate_update = db.state.last_update + charge_update = db.tanks.last_update - if (charge_update > 0) and (rate_update > 0) then + local has_data = build_update > 0 and rate_update > 0 and charge_update > 0 + + if matrix.is_faulted() then + -- a fault occured, cannot reliably update stats + has_data = false + self.im_stat_init = false + self.imtx_faulted_times = { build_update, rate_update, charge_update } + elseif not self.im_stat_init then + -- prevent operation with partially invalid data + -- all fields must have updated since the last fault + has_data = self.imtx_faulted_times[1] < build_update and + self.imtx_faulted_times[2] < rate_update and + self.imtx_faulted_times[3] < charge_update + end + + if has_data 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) @@ -349,6 +367,10 @@ function facility.new(config, cooling_conf) self.imtx_last_charge = energy self.imtx_last_charge_t = charge_update end + else + -- prevent use by control systems + rate_update = 0 + charge_update = 0 end else self.im_stat_init = false @@ -507,7 +529,7 @@ function facility.new(config, cooling_conf) self.status_text = { "CHARGE MODE", "running control loop" } log.info("FAC: CHARGE mode starting PID control") - elseif self.last_update ~= charge_update then + elseif self.last_update < charge_update then -- convert to kFE to make constants not microscopic local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000 @@ -581,7 +603,7 @@ function facility.new(config, cooling_conf) self.status_text = { "GENERATION MODE", "running control loop" } log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control") end - elseif self.last_update ~= rate_update then + elseif self.last_update < rate_update then -- convert to MFE (in rounded kFE) to make constants not microscopic local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000 diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 0c78ff7a..86fd13b4 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.9" +local SUPERVISOR_VERSION = "v1.3.10" local println = util.println local println_ts = util.println_ts