diff --git a/install_manifest.json b/install_manifest.json index 910a948..b7201b0 100644 --- a/install_manifest.json +++ b/install_manifest.json @@ -2,9 +2,9 @@ "versions": { "bootloader": "0.2", "comms": "1.3.3", - "reactor-plc": "beta-v0.10.11", + "reactor-plc": "beta-v0.11.0", "rtu": "beta-v0.11.1", - "supervisor": "beta-v0.12.0", + "supervisor": "beta-v0.12.1", "coordinator": "beta-v0.10.0", "pocket": "alpha-v0.0.0" }, @@ -177,12 +177,12 @@ }, "sizes": { "system": 1982, - "common": 88049, + "common": 88021, "graphics": 99360, "lockbox": 100797, - "reactor-plc": 75915, - "rtu": 81676, - "supervisor": 265030, + "reactor-plc": 75902, + "rtu": 81679, + "supervisor": 267633, "coordinator": 180849, "pocket": 335 } diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5e7297c..f6bbe5d 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -29,7 +29,7 @@ local PCALL_START_MSG = "pcall: Reactor is already active." local MAX_DAMAGE_PERCENT = 90 local MAX_DAMAGE_TEMPERATURE = 1200 -local MIN_COOLANT_FILL = 0.02 +local MIN_COOLANT_FILL = 0.10 local MAX_WASTE_FILL = 0.8 local MAX_HEATED_COLLANT_FILL = 0.95 @@ -206,7 +206,7 @@ function plc.rps_init(reactor, is_formed) self.state[state_keys.manual] = true end - -- automatic SCRAM commanded by supervisor/coordinator + -- automatic SCRAM commanded by supervisor function public.trip_auto() self.state[state_keys.automatic] = true end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 52b7229..4380c33 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.10.11" +local R_PLC_VERSION = "beta-v0.11.0" local print = util.print local println = util.println diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index ed17a3f..79ced10 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -299,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.U_ACK) then + if not util.is_int(port) then return nil elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then return nil diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 8e8246e..fa51cba 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.12.0" +local SUPERVISOR_VERSION = "beta-v0.12.1" local print = util.print local println = util.println diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 258b297..9b0849c 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -82,7 +82,10 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- redstone control io_ctl = nil, ---@type rs_controller valves = {}, ---@type unit_valves + emcool_opened = false, -- auto control + auto_engaged = false, + auto_was_alarmed = false, ramp_target_br100 = 0, -- state tracking deltas = {}, @@ -141,25 +144,25 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- radiation monitor alarm for this unit ContainmentRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ContainmentRadiation, tier = PRIO.CRITICAL }, -- reactor offline after being online - ReactorLost = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorLost, tier = PRIO.URGENT }, + ReactorLost = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorLost, tier = PRIO.TIMELY }, -- damage >100% CriticalDamage = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.CriticalDamage, tier = PRIO.CRITICAL }, -- reactor damage increasing ReactorDamage = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorDamage, tier = PRIO.EMERGENCY }, -- reactor >1200K ReactorOverTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorOverTemp, tier = PRIO.URGENT }, - -- reactor >1100K - ReactorHighTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighTemp, tier = PRIO.TIMELY }, + -- reactor >1150K + ReactorHighTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 1, id = ALARM.ReactorHighTemp, tier = PRIO.TIMELY }, -- waste = 100% ReactorWasteLeak = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorWasteLeak, tier = PRIO.EMERGENCY }, -- waste >85% - ReactorHighWaste = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighWaste, tier = PRIO.TIMELY }, + ReactorHighWaste = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighWaste, tier = PRIO.URGENT }, -- RPS trip occured - RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 1, id = ALARM.RPSTransient, tier = PRIO.TIMELY }, + RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, 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" - TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 1, id = ALARM.TurbineTrip, tier = PRIO.URGENT } + TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.TurbineTrip, tier = PRIO.URGENT } }, ---@class unit_db db = { @@ -488,12 +491,17 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- update alarm status logic.update_alarms(self) + -- if in auto mode, SCRAM on certain alarms + logic.update_auto_safety(public, self) + -- update status text logic.update_status_text(self) -- handle redstone I/O if #self.redstone > 0 then logic.handle_redstone(self) + elseif not self.plc_cache.rps_trip then + self.emcool_opened = false end end @@ -502,7 +510,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- engage automatic control function public.a_engage() - self.db.annunciator.AutoControl = true + self.auto_engaged = true if self.plc_i ~= nil then self.plc_i.auto_lock(true) end @@ -510,7 +518,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- disengage automatic control function public.a_disengage() - self.db.annunciator.AutoControl = false + self.auto_engaged = false if self.plc_i ~= nil then self.plc_i.auto_lock(false) self.db.control.br100 = 0 @@ -533,7 +541,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- set the automatic burn rate based on the last set burn rate in 100ths ---@param ramp boolean true to ramp to rate, false to set right away function public.a_commit_br100(ramp) - if self.db.annunciator.AutoControl then + if self.auto_engaged then if self.plc_i ~= nil then self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp) @@ -562,7 +570,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- queue a command to clear timeout/auto-scram if set function public.a_cond_rps_reset() - if self.plc_s ~= nil and self.plc_i ~= nil then + if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then local rps = self.plc_i.get_rps() if rps.timeout or rps.automatic then self.plc_i.auto_lock(true) -- if it timed out/restarted, auto lock was lost, so re-lock it @@ -668,8 +676,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- check if a critical alarm is tripped function public.has_critical_alarm() - for _, data in pairs(self.alarms) do - if data.tier == PRIO.CRITICAL and (data.state == AISTATE.TRIPPED or data.state == AISTATE.ACKED) then + for _, alarm in pairs(self.alarms) do + if alarm.tier == PRIO.CRITICAL and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then return true end end diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 0709428..836dad2 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -5,6 +5,7 @@ local util = require("scada-common.util") local plc = require("supervisor.session.plc") +local PRIO = types.ALARM_PRIORITY local ALARM_STATE = types.ALARM_STATE local TRI_FAIL = types.TRI_FAIL @@ -50,6 +51,8 @@ function logic.update_annunciator(self) -- REACTOR -- ------------- + self.db.annunciator.AutoControl = self.auto_engaged + -- check PLC status self.db.annunciator.PLCOnline = self.plc_i ~= nil @@ -109,7 +112,7 @@ function logic.update_annunciator(self) self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.automatic self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.no_cool) self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < -2.0 - self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.5 + self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.4 self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100 self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= 0.01 @@ -515,6 +518,37 @@ function logic.update_alarms(self) end end +-- update the internal automatic safety control performed while in auto control mode +---@param public reactor_unit reactor unit public functions +---@param self _unit_self unit instance +function logic.update_auto_safety(public, self) + local AISTATE = self.types.AISTATE + + if self.auto_engaged then + local alarmed = false + + for _, alarm in pairs(self.alarms) do + if alarm.tier <= PRIO.URGENT and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then + if not self.auto_was_alarmed then + log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.alarm_string[alarm.id], ") [PRIORITY ", + types.alarm_prio_string[alarm.tier + 1],"]")) + end + + alarmed = true + break + end + end + + if alarmed and not self.plc_cache.rps_status.automatic then + public.a_scram() + end + + self.auto_was_alarmed = alarmed + else + self.auto_was_alarmed = false + end +end + -- update the two unit status text messages ---@param self _unit_self unit instance function logic.update_status_text(self) @@ -570,6 +604,8 @@ function logic.update_status_text(self) self.status_text = { "WASTE LEVEL HIGH", "waste accumulating in reactor" } elseif is_active(self.alarms.TurbineTrip) then self.status_text = { "TURBINE TRIP", "turbine stall occured" } + elseif self.emcool_opened then + self.status_text = { "EMERGENCY COOLANT OPENED", "reset RPS to close valve" } -- connection dependent states elseif self.plc_i ~= nil then local plc_db = self.plc_i.get_db() @@ -641,6 +677,15 @@ end -- handle unit redstone I/O ---@param self _unit_self unit instance function logic.handle_redstone(self) + local AISTATE = self.types.AISTATE + + -- check if an alarm is active (tripped or ack'd) + ---@param alarm table alarm entry + ---@return boolean active + local function is_active(alarm) + return alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED + end + -- 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 @@ -653,7 +698,7 @@ function logic.handle_redstone(self) 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 + if (not self.auto_engaged) 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) @@ -671,7 +716,7 @@ function logic.handle_redstone(self) -- 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_AUTO_CTRL, self.auto_engaged) 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) @@ -695,13 +740,32 @@ function logic.handle_redstone(self) 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 + ----------------------- + -- Emergency Coolant -- + ----------------------- + + local enable_emer_cool = self.plc_cache.rps_status.no_cool or + (self.auto_engaged and self.db.annunciator.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp)) + + if 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 + -- if auto control is engaged, alarm check will SCRAM on reactor over temp so that's covered self.valves.emer_cool.close() + + if self.db.annunciator.EmergencyCoolant > 1 and self.emcool_opened then + log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed")) + end + + self.emcool_opened = false + elseif enable_emer_cool or self.emcool_opened then + self.valves.emer_cool.open() + + if self.db.annunciator.EmergencyCoolant > 1 and not self.emcool_opened then + log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened")) + end + + self.emcool_opened = true end end