From d9efd5b8d2851f353916218dcad39c2e6fe5c855 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 20 Apr 2024 16:32:18 -0400 Subject: [PATCH] #412 updates to RSIO for induction matrix low, high, and analog charge level --- rtu/configure.lua | 75 ++++++------ rtu/startup.lua | 2 +- scada-common/constants.lua | 12 ++ scada-common/rsio.lua | 214 ++++++++++++++++------------------- scada-common/util.lua | 2 +- supervisor/facility.lua | 14 ++- supervisor/session/rsctl.lua | 25 +++- supervisor/startup.lua | 2 +- test/rstest.lua | 79 ++++++------- 9 files changed, 226 insertions(+), 199 deletions(-) diff --git a/rtu/configure.lua b/rtu/configure.lua index 7758dc2..5dc440a 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -2,6 +2,7 @@ -- Configuration GUI -- +local constants = require("scada-common.constants") local log = require("scada-common.log") local ppm = require("scada-common.ppm") local rsio = require("scada-common.rsio") @@ -39,39 +40,42 @@ local CENTER = core.ALIGN.CENTER local RIGHT = core.ALIGN.RIGHT -- rsio port descriptions -local PORT_DESC = { - "Facility SCRAM", - "Facility Acknowledge", - "Reactor SCRAM", - "Reactor RPS Reset", - "Reactor Enable", - "Unit Acknowledge", - "Facility Alarm (high prio)", - "Facility Alarm (any)", - "Waste Plutonium Valve", - "Waste Polonium Valve", - "Waste Po Pellets Valve", - "Waste Antimatter Valve", - "Reactor Active", - "Reactor in Auto Control", - "RPS Tripped", - "RPS Auto SCRAM", - "RPS High Damage", - "RPS High Temperature", - "RPS Low Coolant", - "RPS Excess Heated Coolant", - "RPS Excess Waste", - "RPS Insufficient Fuel", - "RPS PLC Fault", - "RPS Supervisor Timeout", - "Unit Alarm", - "Unit Emergency Cool. Valve" +local PORT_DESC_MAP = { + { IO.F_SCRAM, "Facility SCRAM" }, + { IO.F_ACK, "Facility Acknowledge" }, + { IO.R_SCRAM, "Reactor SCRAM" }, + { IO.R_RESET, "Reactor RPS Reset" }, + { IO.R_ENABLE, "Reactor Enable" }, + { IO.U_ACK, "Unit Acknowledge" }, + { IO.F_ALARM, "Facility Alarm (high prio)" }, + { IO.F_ALARM_ANY, "Facility Alarm (any)" }, + { IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" }, + { IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" }, + { IO.F_MATRIX_CHG, "Induction Matrix Charge %" }, + { IO.WASTE_PU, "Waste Plutonium Valve" }, + { IO.WASTE_PO, "Waste Polonium Valve" }, + { IO.WASTE_POPL, "Waste Po Pellets Valve" }, + { IO.WASTE_AM, "Waste Antimatter Valve" }, + { IO.R_ACTIVE, "Reactor Active" }, + { IO.R_AUTO_CTRL, "Reactor in Auto Control" }, + { IO.R_SCRAMMED, "RPS Tripped" }, + { IO.R_AUTO_SCRAM, "RPS Auto SCRAM" }, + { IO.R_HIGH_DMG, "RPS High Damage" }, + { IO.R_HIGH_TEMP, "RPS High Temperature" }, + { IO.R_LOW_COOLANT, "RPS Low Coolant" }, + { IO.R_EXCESS_HC, "RPS Excess Heated Coolant" }, + { IO.R_EXCESS_WS, "RPS Excess Waste" }, + { IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" }, + { IO.R_PLC_FAULT, "RPS PLC Fault" }, + { IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" }, + { IO.U_ALARM, "Unit Alarm" }, + { IO.U_EMER_COOL, "Unit Emergency Cool. Valve" } } -- designation (0 = facility, 1 = unit) -local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } +local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 } -assert(#PORT_DESC == rsio.NUM_PORTS) +assert(#PORT_DESC_MAP == rsio.NUM_PORTS) assert(#PORT_DSGN == rsio.NUM_PORTS) -- changes to the config data/format to let the user know @@ -1167,14 +1171,17 @@ local function config_view(display) PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)} TextBox{parent=all_w_macro,x=16,y=1,width=5,height=1,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=all_w_macro,x=22,y=1,height=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)} + for i = 1, rsio.NUM_PORTS do - local name = rsio.to_string(i) - local io_dir = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, "[in]", "[out]") - local btn_color = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) + local p = PORT_DESC_MAP[i][1] + local name = rsio.to_string(p) + local io_dir = util.trinary(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]") + local btn_color = util.trinary(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue) + local entry = Div{parent=rs_ports,height=1} - PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(i)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} + PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)} TextBox{parent=entry,x=16,y=1,width=5,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC[i],fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)} end PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} diff --git a/rtu/startup.lua b/rtu/startup.lua index 9470124..027549c 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.9.4" +local RTU_VERSION = "v1.9.5" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE diff --git a/scada-common/constants.lua b/scada-common/constants.lua index 20925bd..678ea98 100644 --- a/scada-common/constants.lua +++ b/scada-common/constants.lua @@ -66,6 +66,18 @@ constants.ALARM_LIMITS = alarms --#endregion +--#region Supervisor Redstone Activation Thresholds + +---@class _rs_threshold_constants +local rs = {} + +rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW +rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH + +constants.RS_THRESHOLDS = rs + +--#endregion + --#region Supervisor Constants -- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 6d1e688..8f4c558 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -52,6 +52,8 @@ local IO_PORT = { -- facility F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm) F_ALARM_ANY = 8, -- active high, any alarm regardless of priority + F_MATRIX_LOW = 27, -- active high, induction matrix charge less than + F_MATRIX_HIGH = 28, -- active high, induction matrix charge high -- waste WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route @@ -75,17 +77,27 @@ local IO_PORT = { -- unit outputs U_ALARM = 25, -- active high, unit alarm - U_EMER_COOL = 26 -- active low, emergency coolant control + U_EMER_COOL = 26, -- active low, emergency coolant control + + -- analog outputs -- + + -- facility + F_MATRIX_CHG = 29 -- analog charge level of the induction matrix } rsio.IO_LVL = IO_LVL rsio.IO_DIR = IO_DIR rsio.IO_MODE = IO_MODE rsio.IO = IO_PORT -rsio.NUM_PORTS = IO_PORT.U_EMER_COOL + +rsio.NUM_PORTS = 29 +rsio.NUM_DIG_PORTS = 28 +rsio.NUM_ANA_PORTS = 1 -- self checks +assert(rsio.NUM_PORTS == (rsio.NUM_DIG_PORTS + rsio.NUM_ANA_PORTS), "port counts inconsistent") + local dup_chk = {} for _, v in pairs(IO_PORT) do assert(dup_chk[v] ~= true, "duplicate in port list") @@ -96,64 +108,45 @@ assert(#dup_chk == rsio.NUM_PORTS, "port list malformed") --#endregion ---#region Utility Functions +--#region Utility Functions and Attribute Tables -local PORT_NAMES = { - "F_SCRAM", - "F_ACK", - "R_SCRAM", - "R_RESET", - "R_ENABLE", - "U_ACK", - "F_ALARM", - "F_ALARM_ANY", - "WASTE_PU", - "WASTE_PO", - "WASTE_POPL", - "WASTE_AM", - "R_ACTIVE", - "R_AUTO_CTRL", - "R_SCRAMMED", - "R_AUTO_SCRAM", - "R_HIGH_DMG", - "R_HIGH_TEMP", - "R_LOW_COOLANT", - "R_EXCESS_HC", - "R_EXCESS_WS", - "R_INSUFF_FUEL", - "R_PLC_FAULT", - "R_PLC_TIMEOUT", - "U_ALARM", - "U_EMER_COOL" -} +local IO = IO_PORT +-- list of all port names +local PORT_NAMES = {} +for k, v in pairs(IO) do PORT_NAMES[v] = k end + +-- list of all port I/O modes local MODES = { - IO_MODE.DIGITAL_IN, -- F_SCRAM - IO_MODE.DIGITAL_IN, -- F_ACK - IO_MODE.DIGITAL_IN, -- R_SCRAM - IO_MODE.DIGITAL_IN, -- R_RESET - IO_MODE.DIGITAL_IN, -- R_ENABLE - IO_MODE.DIGITAL_IN, -- U_ACK - IO_MODE.DIGITAL_OUT, -- F_ALARM - IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY - IO_MODE.DIGITAL_OUT, -- WASTE_PU - IO_MODE.DIGITAL_OUT, -- WASTE_PO - IO_MODE.DIGITAL_OUT, -- WASTE_POPL - IO_MODE.DIGITAL_OUT, -- WASTE_AM - IO_MODE.DIGITAL_OUT, -- R_ACTIVE - IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL - IO_MODE.DIGITAL_OUT, -- R_SCRAMMED - IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM - IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG - IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP - IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT - IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC - IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS - IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL - IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT - IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT - IO_MODE.DIGITAL_OUT, -- U_ALARM - IO_MODE.DIGITAL_OUT -- U_EMER_COOL + [IO.F_SCRAM] = IO_MODE.DIGITAL_IN, + [IO.F_ACK] = IO_MODE.DIGITAL_IN, + [IO.R_SCRAM] = IO_MODE.DIGITAL_IN, + [IO.R_RESET] = IO_MODE.DIGITAL_IN, + [IO.R_ENABLE] = IO_MODE.DIGITAL_IN, + [IO.U_ACK] = IO_MODE.DIGITAL_IN, + [IO.F_ALARM] = IO_MODE.DIGITAL_OUT, + [IO.F_ALARM_ANY] = IO_MODE.DIGITAL_OUT, + [IO.F_MATRIX_LOW] = IO_MODE.DIGITAL_OUT, + [IO.F_MATRIX_HIGH] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_PU] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_PO] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_POPL] = IO_MODE.DIGITAL_OUT, + [IO.WASTE_AM] = IO_MODE.DIGITAL_OUT, + [IO.R_ACTIVE] = IO_MODE.DIGITAL_OUT, + [IO.R_AUTO_CTRL] = IO_MODE.DIGITAL_OUT, + [IO.R_SCRAMMED] = IO_MODE.DIGITAL_OUT, + [IO.R_AUTO_SCRAM] = IO_MODE.DIGITAL_OUT, + [IO.R_HIGH_DMG] = IO_MODE.DIGITAL_OUT, + [IO.R_HIGH_TEMP] = IO_MODE.DIGITAL_OUT, + [IO.R_LOW_COOLANT] = IO_MODE.DIGITAL_OUT, + [IO.R_EXCESS_HC] = IO_MODE.DIGITAL_OUT, + [IO.R_EXCESS_WS] = IO_MODE.DIGITAL_OUT, + [IO.R_INSUFF_FUEL] = IO_MODE.DIGITAL_OUT, + [IO.R_PLC_FAULT] = IO_MODE.DIGITAL_OUT, + [IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT, + [IO.U_ALARM] = IO_MODE.DIGITAL_OUT, + [IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT, + [IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT } assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect") @@ -179,74 +172,51 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur -- I/O mappings to I/O function and I/O mode local RS_DIO_MAP = { - -- F_SCRAM - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, - -- F_ACK - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.F_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, + [IO.F_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- R_SCRAM - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, - -- R_RESET - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- R_ENABLE - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.R_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN }, + [IO.R_RESET] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.R_ENABLE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- U_ACK - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, + [IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN }, - -- F_ALARM - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- F_ALARM_ANY - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_ALARM_ANY] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_MATRIX_LOW] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.F_MATRIX_HIGH] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- WASTE_PU - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- WASTE_PO - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- WASTE_POPL - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- WASTE_AM - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_PU] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_PO] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_POPL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, + [IO.WASTE_AM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, - -- R_ACTIVE - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_AUTO_CTRL - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_SCRAMMED - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_AUTO_SCRAM - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_HIGH_DMG - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_HIGH_TEMP - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_LOW_COOLANT - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_EXCESS_HC - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_EXCESS_WS - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_INSUFF_FUEL - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_PLC_FAULT - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- R_PLC_TIMEOUT - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_ACTIVE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_AUTO_CTRL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_SCRAMMED] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_AUTO_SCRAM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_HIGH_DMG] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_HIGH_TEMP] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_LOW_COOLANT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_EXCESS_HC] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_EXCESS_WS] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_INSUFF_FUEL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_PLC_FAULT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- U_ALARM - { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, - -- U_EMER_COOL - { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT } + [IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, + [IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT } } -assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect") +assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect") -- get the I/O direction of a port ---@nodiscard ---@param port IO_PORT ---@return IO_DIR function rsio.get_io_dir(port) - if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode + if rsio.is_valid_port(port) then + return util.trinary(MODES[port] == IO_MODE.DIGITAL_OUT or MODES[port] == IO_MODE.ANALOG_OUT, IO_DIR.OUT, IO_DIR.IN) else return IO_DIR.IN end end @@ -310,6 +280,13 @@ end --#region Digital I/O +-- check if a port is digital +---@nodiscard +---@param port IO_PORT +function rsio.is_digital(port) + return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.DIGITAL_IN or MODES[port] == IO_MODE.DIGITAL_OUT) +end + -- get digital I/O level reading from a redstone boolean input value ---@nodiscard ---@param rs_value boolean raw value from redstone @@ -330,7 +307,7 @@ function rsio.digital_write(level) return level == IO_LVL.HIGH end ---@param active boolean state to convert to logic level ---@return IO_LVL|false function rsio.digital_write_active(port, active) - if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then + if not rsio.is_digital(port) then return false else return RS_DIO_MAP[port]._out(active) @@ -343,9 +320,7 @@ end ---@param level IO_LVL logic level ---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided function rsio.digital_is_active(port, level) - if not util.is_int(port) then - return nil - elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then + if (not rsio.is_digital(port)) or level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then return nil else return RS_DIO_MAP[port]._in(level) @@ -356,6 +331,13 @@ end --#region Analog I/O +-- check if a port is analog +---@nodiscard +---@param port IO_PORT +function rsio.is_analog(port) + return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.ANALOG_IN or MODES[port] == IO_MODE.ANALOG_OUT) +end + -- read an analog value scaled from min to max ---@nodiscard ---@param rs_value number redstone reading (0 to 15) @@ -372,7 +354,7 @@ end ---@param value number value to write (from min to max range) ---@param min number minimum of range ---@param max number maximum of range ----@return number rs_value scaled redstone reading (0 to 15) +---@return integer rs_value scaled redstone reading (0 to 15) function rsio.analog_write(value, min, max) local scaled_value = (value - min) / (max - min) return math.floor(scaled_value * 15) diff --git a/scada-common/util.lua b/scada-common/util.lua index 98a1667..2f92424 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.2.2" +util.version = "1.3.0" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 33cd267..d7058a8 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -300,8 +300,8 @@ function facility.new(config, cooling_conf) -- calculate moving averages for induction matrix if self.induction[1] ~= nil then - local matrix = self.induction[1] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local matrix = self.induction[1] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db charge_update = db.tanks.last_update rate_update = db.state.last_update @@ -774,6 +774,16 @@ function facility.new(config, cooling_conf) self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm) self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm) + + -- update induction matrix related outputs + if self.induction[1] ~= nil then + local matrix = self.induction[1] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db + + self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) + self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) + self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1) + end end --#endregion diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua index 1bdef5a..a369937 100644 --- a/supervisor/session/rsctl.lua +++ b/supervisor/session/rsctl.lua @@ -2,6 +2,8 @@ -- Redstone RTU Session I/O Controller -- +local rsio = require("scada-common.rsio") + local rsctl = {} -- create a new redstone RTU I/O controller @@ -16,7 +18,7 @@ function rsctl.new(redstone_rtus) ---@return boolean function public.is_connected(port) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local db = redstone_rtus[i].get_db() ---@type redstone_session_db if db.io[port] ~= nil then return true end end @@ -28,8 +30,8 @@ function rsctl.new(redstone_rtus) ---@param value boolean function public.digital_write(port, value) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil if io ~= nil then io.write(value) end end end @@ -40,12 +42,25 @@ function rsctl.new(redstone_rtus) ---@return boolean|nil function public.digital_read(port) for i = 1, #redstone_rtus do - local db = redstone_rtus[i].get_db() ---@type redstone_session_db - local io = db.io[port] ---@type rs_db_dig_io|nil + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_dig_io|nil if io ~= nil then return io.read() end end end + -- write to an analog redstone port (applies to all RTUs) + ---@param port IO_PORT + ---@param value number value + ---@param min number minimum value for scaling 0 to 15 + ---@param max number maximum value for scaling 0 to 15 + function public.analog_write(port, value, min, max) + for i = 1, #redstone_rtus do + local db = redstone_rtus[i].get_db() ---@type redstone_session_db + local io = db.io[port] ---@type rs_db_ana_io|nil + if io ~= nil then io.write(rsio.analog_write(value, min, max)) end + end + end + return public end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 1694ebe..2a2202c 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.6" +local SUPERVISOR_VERSION = "v1.3.7" local println = util.println local println_ts = util.println_ts diff --git a/test/rstest.lua b/test/rstest.lua index e322e28..ba0b156 100644 --- a/test/rstest.lua +++ b/test/rstest.lua @@ -1,16 +1,28 @@ require("/initenv").init_env() -local rsio = require("scada-common.rsio") -local util = require("scada-common.util") +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") local testutils = require("test.testutils") +local IO = rsio.IO +local IO_LVL = rsio.IO_LVL +local IO_MODE = rsio.IO_MODE + local print = util.print local println = util.println -local IO = rsio.IO -local IO_LVL = rsio.IO_LVL -local IO_MODE = rsio.IO_MODE +-- list of inverted digital signals
+-- just using the key for a quick lookup, value need to be not nil +local DIG_INV = { + [IO.F_SCRAM] = 0, + [IO.R_SCRAM] = 0, + [IO.WASTE_PU] = 0, + [IO.WASTE_PO] = 0, + [IO.WASTE_POPL] = 0, + [IO.WASTE_AM] = 0, + [IO.U_EMER_COOL] = 0 +} println("starting RSIO tester") println("") @@ -50,8 +62,8 @@ testutils.pause() println(">>> checking invalid ports:") -testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "") -testutils.test_func_nil("rsio.to_string", rsio.to_string, "") +testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "UNKNOWN") +testutils.test_func_nil("rsio.to_string", rsio.to_string, "UNKNOWN") testutils.test_func("rsio.get_io_mode", rsio.get_io_mode, { -1, 100, false }, IO_MODE.ANALOG_IN) testutils.test_func_nil("rsio.get_io_mode", rsio.get_io_mode, IO_MODE.ANALOG_IN) @@ -100,46 +112,35 @@ println(">>> checking port I/O:") print("rsio.digital_is_active(...): ") --- check input ports -assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.LOW) == true, "IO_F_SCRAM_HIGH") -assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.HIGH) == false, "IO_F_SCRAM_LOW") -assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.LOW) == true, "IO_R_SCRAM_HIGH") -assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.HIGH) == false, "IO_R_SCRAM_LOW") -assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.LOW) == false, "IO_R_ENABLE_HIGH") -assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.HIGH) == true, "IO_R_ENABLE_LOW") +-- check all digital ports +for i = 1, rsio.NUM_PORTS do + if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then + local high = DIG_INV[i] == nil + assert(rsio.digital_is_active(i, IO_LVL.LOW) == not high, "IO_" .. rsio.to_string(i) .. "_LOW") + assert(rsio.digital_is_active(i, IO_LVL.HIGH) == high, "IO_" .. rsio.to_string(i) .. "_HIGH") + end +end --- non-inputs should always return LOW -assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.LOW) == false, "IO_OUT_READ_LOW") -assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.HIGH) == false, "IO_OUT_READ_HIGH") +assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.LOW) == nil, "ANA_DIG_READ_LOW") +assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.HIGH) == nil, "ANA_DIG_READ_HIGH") println("PASS") --- check output ports +-- check digital write -print("rsio.digital_write(...): ") +print("rsio.digital_write_active(...): ") --- check output ports -assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.LOW, "IO_F_ALARM_LOW") -assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_HIGH") -assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.HIGH, "IO_WASTE_PU_HIGH") -assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_LOW") -assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.HIGH, "IO_WASTE_PO_HIGH") -assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_LOW") -assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.HIGH, "IO_WASTE_POPL_HIGH") -assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.LOW, "IO_WASTE_POPL_LOW") -assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.HIGH, "IO_WASTE_AM_HIGH") -assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_LOW") - --- check all reactor output ports (all are active high) -for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do - assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_PORT") - assert(rsio.digital_write_active(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW") - assert(rsio.digital_write_active(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH") +-- check all digital ports +for i = 1, rsio.NUM_PORTS do + if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then + local high = DIG_INV[i] == nil + assert(rsio.digital_write_active(i, not high) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW") + assert(rsio.digital_write_active(i, high) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH") + end end --- non-outputs should always return false -assert(rsio.digital_write_active(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_FALSE") -assert(rsio.digital_write_active(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_TRUE") +assert(rsio.digital_write_active(IO.F_MATRIX_CHG, true) == false, "ANA_DIG_WRITE_TRUE") +assert(rsio.digital_write_active(IO.F_MATRIX_CHG, false) == false, "ANA_DIG_WRITE_FALSE") println("PASS")