From cc5ea0dbb0b951ccaccd6f61e6fdd929d47f6192 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 19 Feb 2023 00:14:27 -0500 Subject: [PATCH] #159 linked up redstone I/O --- rtu/startup.lua | 2 +- scada-common/rsio.lua | 101 +++++++++++++++++++++++---------------- supervisor/facility.lua | 44 +++++++++++++++-- supervisor/startup.lua | 2 +- supervisor/unit.lua | 44 +++++++++++------ supervisor/unitlogic.lua | 71 +++++++++++++++++++++++++++ 6 files changed, 201 insertions(+), 63 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 375e93f..bbe5ee8 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,7 +25,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 = "beta-v0.10.6" +local RTU_VERSION = "beta-v0.11.0" local rtu_t = types.rtu_t diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 7736a90..ed17a3f 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -11,7 +11,7 @@ local rsio = {} -- RS I/O CONSTANTS -- ---------------------- ----@alias IO_LVL integer +---@enum IO_LVL I/O logic level local IO_LVL = { DISCONNECT = -1, -- use for RTU session to indicate this RTU is not connected to this port LOW = 0, @@ -19,13 +19,13 @@ local IO_LVL = { FLOATING = 2 -- use for RTU session to indicate this RTU is connected but not yet read } ----@alias IO_DIR integer +---@enum IO_DIR I/O direction local IO_DIR = { IN = 0, OUT = 1 } ----@alias IO_MODE integer +---@enum IO_MODE I/O mode (digital/analog input/output) local IO_MODE = { DIGITAL_IN = 0, DIGITAL_OUT = 1, @@ -33,45 +33,50 @@ local IO_MODE = { ANALOG_OUT = 3 } ----@alias IO_PORT integer +---@enum IO_PORT redstone I/O logic port local IO_PORT = { -- digital inputs -- -- facility F_SCRAM = 1, -- active low, facility-wide scram + F_ACK = 2, -- active high, facility alarm acknowledge -- reactor - R_SCRAM = 2, -- active low, reactor scram - R_ENABLE = 3, -- active high, reactor enable + R_SCRAM = 3, -- active low, reactor scram + R_RESET = 4, -- active high, reactor RPS reset + R_ENABLE = 5, -- active high, reactor enable + + -- unit + U_ACK = 6, -- active high, unit alarm acknowledge -- digital outputs -- -- facility - F_ALARM = 4, -- active high, facility safety alarm + F_ALARM = 7, -- active high, facility alarm (any high priority unit alarm) -- waste - WASTE_PU = 5, -- active low, waste -> plutonium -> pellets route - WASTE_PO = 6, -- active low, waste -> polonium route - WASTE_POPL = 7, -- active low, polonium -> pellets route - WASTE_AM = 8, -- active low, polonium -> anti-matter route + WASTE_PU = 8, -- active low, waste -> plutonium -> pellets route + WASTE_PO = 9, -- active low, waste -> polonium route + WASTE_POPL = 10, -- active low, polonium -> pellets route + WASTE_AM = 11, -- active low, polonium -> anti-matter route -- reactor - R_ALARM = 9, -- active high, reactor safety alarm - R_SCRAMMED = 10, -- active high, if the reactor is scrammed - R_AUTO_SCRAM = 11, -- active high, if the reactor was automatically scrammed R_ACTIVE = 12, -- active high, if the reactor is active R_AUTO_CTRL = 13, -- active high, if the reactor burn rate is automatic - R_DMG_CRIT = 14, -- active high, if the reactor damage is critical - R_HIGH_TEMP = 15, -- active high, if the reactor is at a high temperature - R_NO_COOLANT = 16, -- active high, if the reactor has no coolant - R_EXCESS_HC = 17, -- active high, if the reactor has excess heated coolant - R_EXCESS_WS = 18, -- active high, if the reactor has excess waste - R_INSUFF_FUEL = 19, -- active high, if the reactor has insufficent fuel - R_PLC_FAULT = 20, -- active high, if the reactor PLC reports a device access fault - R_PLC_TIMEOUT = 21, -- active high, if the reactor PLC has not been heard from + R_SCRAMMED = 14, -- active high, if the reactor is scrammed + R_AUTO_SCRAM = 15, -- active high, if the reactor was automatically scrammed + R_DMG_CRIT = 16, -- active high, if the reactor damage is critical + R_HIGH_TEMP = 17, -- active high, if the reactor is at a high temperature + R_NO_COOLANT = 18, -- active high, if the reactor has no coolant + R_EXCESS_HC = 19, -- active high, if the reactor has excess heated coolant + R_EXCESS_WS = 20, -- active high, if the reactor has excess waste + R_INSUFF_FUEL = 21, -- active high, if the reactor has insufficent fuel + R_PLC_FAULT = 22, -- active high, if the reactor PLC reports a device access fault + R_PLC_TIMEOUT = 23, -- active high, if the reactor PLC has not been heard from -- unit outputs - U_EMER_COOL = 22 -- active low, emergency coolant control + U_ALARM = 24, -- active high, unit alarm + U_EMER_COOL = 25 -- active low, emergency coolant control } rsio.IO_LVL = IO_LVL @@ -88,18 +93,20 @@ rsio.IO = IO_PORT function rsio.to_string(port) local names = { "F_SCRAM", + "F_ACK", "R_SCRAM", + "R_RESET", "R_ENABLE", + "U_ACK", "F_ALARM", "WASTE_PU", "WASTE_PO", "WASTE_POPL", "WASTE_AM", - "R_ALARM", - "R_SCRAMMED", - "R_AUTO_SCRAM", "R_ACTIVE", "R_AUTO_CTRL", + "R_SCRAMMED", + "R_AUTO_SCRAM", "R_DMG_CRIT", "R_HIGH_TEMP", "R_NO_COOLANT", @@ -108,6 +115,7 @@ function rsio.to_string(port) "R_INSUFF_FUEL", "R_PLC_FAULT", "R_PLC_TIMEOUT", + "U_ALARM", "U_EMER_COOL" } @@ -129,12 +137,22 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur 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 }, + -- WASTE_PU { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_PO @@ -143,16 +161,15 @@ local RS_DIO_MAP = { { _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_ALARM + + -- 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_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_DMG_CRIT { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_HIGH_TEMP @@ -169,6 +186,9 @@ local RS_DIO_MAP = { { _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 } } @@ -179,18 +199,20 @@ local RS_DIO_MAP = { function rsio.get_io_mode(port) 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, -- 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_ALARM - IO_MODE.DIGITAL_OUT, -- R_SCRAMMED - IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM 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_DMG_CRIT IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP IO_MODE.DIGITAL_OUT, -- R_NO_COOLANT @@ -199,6 +221,7 @@ function rsio.get_io_mode(port) 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 } @@ -249,11 +272,7 @@ end ---@param rs_value boolean ---@return IO_LVL function rsio.digital_read(rs_value) - if rs_value then - return IO_LVL.HIGH - else - return IO_LVL.LOW - end + if rs_value then return IO_LVL.HIGH else return IO_LVL.LOW end end -- get redstone boolean output value corresponding to a digital I/O level @@ -280,7 +299,7 @@ end ---@param level IO_LVL ---@return boolean|nil function rsio.digital_is_active(port, level) - if (not util.is_int(port)) or (port > IO_PORT.R_ENABLE) then + if (not util.is_int(port)) or (port > IO_PORT.U_ACK) then return nil elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then return nil @@ -310,7 +329,7 @@ end ---@return number rs_value scaled redstone reading (0 to 15) function rsio.analog_write(value, min, max) local scaled_value = (value - min) / (max - min) - return scaled_value * 15 + return math.floor(scaled_value * 15) end return rsio diff --git a/supervisor/facility.lua b/supervisor/facility.lua index cff8780..1b2634c 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -6,11 +6,12 @@ local util = require("scada-common.util") local unit = require("supervisor.unit") local rsctl = require("supervisor.session.rsctl") -local unit = require("supervisor.session.unit") local PROCESS = types.PROCESS local PROCESS_NAMES = types.PROCESS_NAMES +local IO = rsio.IO + -- 7.14 kJ per blade for 1 mB of fissile fuel
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum) local POWER_PER_BLADE = util.joules_to_fe(7140) @@ -57,12 +58,15 @@ local facility = {} function facility.new(num_reactors, cooling_conf) local self = { units = {}, + status_text = { "START UP", "initializing..." }, + all_sys_ok = false, + -- rtus + rtu_conn_count = 0, redstone = {}, induction = {}, envd = {}, - status_text = { "START UP", "initializing..." }, - all_sys_ok = false, - rtu_conn_count = 0, + -- redstone I/O control + io_ctl = nil, ---@type rs_controller -- process control units_ready = false, mode = PROCESS.INACTIVE, @@ -111,7 +115,7 @@ function facility.new(num_reactors, cooling_conf) end -- init redstone RTU I/O controller - local rs_rtu_io_ctl = rsctl.new(self.redstone) + self.io_ctl = rsctl.new(self.redstone) -- unlink disconnected units ---@param sessions table @@ -655,6 +659,36 @@ function facility.new(num_reactors, cooling_conf) -- update last mode and set next mode self.last_mode = self.mode self.mode = next_mode + + ------------------------- + -- Handle Redstone I/O -- + ------------------------- + + if #self.redstone > 0 then + -- handle facility SCRAM + if self.io_ctl.digital_read(IO.F_SCRAM) then + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + u.cond_scram() + end + end + + -- handle facility ack + if self.io_ctl.digital_read(IO.F_ACK) then public.ack_all() end + + -- update facility alarm output + local has_alarm = false + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + + if u.has_critical_alarm() then + has_alarm = true + return + end + end + + self.io_ctl.digital_write(IO.F_ALARM, has_alarm) + end end -- call the update function of all units in the facility diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 191ae28..4289b17 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.11.10" +local SUPERVISOR_VERSION = "beta-v0.11.11" local print = util.print local println = util.println diff --git a/supervisor/unit.lua b/supervisor/unit.lua index a18282d..1eff099 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -74,10 +74,14 @@ function unit.new(for_reactor, num_boilers, num_turbines) num_turbines = num_turbines, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, defs = { FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS }, + -- rtus redstone = {}, boilers = {}, turbines = {}, envd = {}, + -- redstone control + io_ctl = nil, ---@type rs_controller + valves = {}, ---@type unit_valves -- auto control ramp_target_br100 = 0, -- state tracking @@ -151,7 +155,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- waste >85% ReactorHighWaste = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighWaste, tier = PRIO.TIMELY }, -- RPS trip occured - RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 1, id = ALARM.RPSTransient, tier = PRIO.URGENT }, + RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 1, id = ALARM.RPSTransient, tier = PRIO.TIMELY }, -- BoilRateMismatch, CoolantFeedMismatch, SteamFeedMismatch, MaxWaterReturnFeed RCSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 5, id = ALARM.RCSTransient, tier = PRIO.TIMELY }, -- "It's just a routine turbin' trip!" -Bill Gibson, "The China Syndrome" @@ -223,7 +227,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) } -- init redstone RTU I/O controller - local rs_rtu_io_ctl = rsctl.new(self.redstone) + self.io_ctl = rsctl.new(self.redstone) -- init boiler table fields for _ = 1, num_boilers do @@ -272,10 +276,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- get the delta t of a value ---@param key string value key - ---@return number - function self._get_dt(key) - if self.deltas[key] then return self.deltas[key].dt else return 0.0 end - end + ---@return number value value or 0 if not known + function self._get_dt(key) if self.deltas[key] then return self.deltas[key].dt else return 0.0 end end -- update all delta computations local function _dt__compute_all() @@ -320,8 +322,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) --#region redstone I/O - local __rs_w = rs_rtu_io_ctl.digital_write - local __rs_r = rs_rtu_io_ctl.digital_read + local __rs_w = self.io_ctl.digital_write + local __rs_r = self.io_ctl.digital_read -- valves local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } @@ -330,6 +332,15 @@ function unit.new(for_reactor, num_boilers, num_turbines) local waste_sps = { open = function () __rs_w(IO.WASTE_AM, true) end, close = function () __rs_w(IO.WASTE_AM, false) end } local emer_cool = { open = function () __rs_w(IO.U_EMER_COOL, true) end, close = function () __rs_w(IO.U_EMER_COOL, false) end } + ---@class unit_valves + self.valves = { + waste_pu = waste_pu, + waste_sna = waste_sna, + waste_po = waste_po, + waste_sps = waste_sps, + emer_cool = emer_cool + } + --#endregion -- unlink disconnected units @@ -480,13 +491,9 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- update status text logic.update_status_text(self) - -- check if emergency coolant is needed - if self.plc_cache.rps_status.no_cool then - emer_cool.open() - elseif not self.plc_cache.rps_trip then - -- can't turn off on sufficient coolant level since it might drop again - -- turn off once system is OK again - emer_cool.close() + -- handle redstone I/O + if #self.redstone > 0 then + logic.handle_redstone(self) end end @@ -576,6 +583,13 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end + -- queue a SCRAM command only if a manual SCRAM has not already occured + function public.cond_scram() + if self.plc_s ~= nil and not self.plc_cache.rps_status.manual then + self.plc_s.in_queue.push_command(PLC_S_CMDS.SCRAM) + end + end + -- acknowledge all alarms (if possible) function public.ack_all() for i = 1, #self.db.alarm_states do diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 647def8..6cece31 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -3,6 +3,8 @@ local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") +local plc = require("supervisor.session.plc") + local ALARM_STATE = types.ALARM_STATE local TRI_FAIL = types.TRI_FAIL @@ -10,6 +12,8 @@ local DUMPING_MODE = types.DUMPING_MODE local IO = rsio.IO +local PLC_S_CMDS = plc.PLC_S_CMDS + local aistate_string = { "INACTIVE", "TRIPPING", @@ -620,4 +624,71 @@ function logic.update_status_text(self) end end +-- handle unit redstone I/O +---@param self _unit_self unit instance +function logic.handle_redstone(self) + -- reactor controls + if self.plc_s ~= nil then + if (not self.plc_cache.rps_status.manual) and self.io_ctl.digital_read(IO.R_SCRAM) then + -- reactor SCRAM requested but not yet done; perform it + self.plc_s.in_queue.push_command(PLC_S_CMDS.SCRAM) + end + + if self.plc_cache.rps_trip and self.io_ctl.digital_read(IO.R_RESET) then + -- reactor RPS reset requested but not yet done; perform it + self.plc_s.in_queue.push_command(PLC_S_CMDS.RPS_RESET) + end + + if (not self.db.annunciator.AutoControl) and (not self.plc_cache.active) and + (not self.plc_cache.rps_trip) and self.io_ctl.digital_read(IO.R_ACTIVE) then + -- reactor enable requested and allowable, but not yet done; perform it + self.plc_s.in_queue.push_command(PLC_S_CMDS.ENABLE) + end + end + + -- check for request to ack all alarms + if self.io_ctl.digital_read(IO.U_ACK) then + for i = 1, #self.db.alarm_states do + if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then + self.db.alarm_states[i] = ALARM_STATE.ACKED + end + end + end + + -- write reactor status outputs + self.io_ctl.digital_write(IO.R_ACTIVE, self.plc_cache.active) + self.io_ctl.digital_write(IO.R_AUTO_CTRL, self.db.annunciator.AutoControl) + self.io_ctl.digital_write(IO.R_SCRAMMED, self.plc_cache.rps_trip) + self.io_ctl.digital_write(IO.R_AUTO_SCRAM, self.plc_cache.rps_status.automatic) + self.io_ctl.digital_write(IO.R_DMG_CRIT, self.plc_cache.rps_status.dmg_crit) + self.io_ctl.digital_write(IO.R_HIGH_TEMP, self.plc_cache.rps_status.high_temp) + self.io_ctl.digital_write(IO.R_NO_COOLANT, self.plc_cache.rps_status.no_cool) + self.io_ctl.digital_write(IO.R_EXCESS_HC, self.plc_cache.rps_status.ex_hcool) + self.io_ctl.digital_write(IO.R_EXCESS_WS, self.plc_cache.rps_status.ex_waste) + self.io_ctl.digital_write(IO.R_INSUFF_FUEL, self.plc_cache.rps_status.no_fuel) + self.io_ctl.digital_write(IO.R_PLC_FAULT, self.plc_cache.rps_status.fault) + self.io_ctl.digital_write(IO.R_PLC_TIMEOUT, self.plc_cache.rps_status.timeout) + + -- write unit outputs + + local has_alarm = false + for i = 1, #self.db.alarm_states do + if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then + has_alarm = true + break + end + end + + self.io_ctl.digital_write(IO.U_ALARM, has_alarm) + + -- check if emergency coolant is needed + if self.plc_cache.rps_status.no_cool then + self.valves.emer_cool.open() + elseif not self.plc_cache.rps_trip then + -- can't turn off on sufficient coolant level since it might drop again + -- turn off once system is OK again + self.valves.emer_cool.close() + end +end + return logic