#572 added facility radiation alarm

This commit is contained in:
Mikayla
2025-04-29 19:41:52 +00:00
parent 04c53c7074
commit eafd39fa35
8 changed files with 204 additions and 140 deletions

View File

@ -465,7 +465,8 @@ types.ALARM = {
ReactorHighWaste = 9,
RPSTransient = 10,
RCSTransient = 11,
TurbineTrip = 12
TurbineTrip = 12,
FacilityRadiation = 13
}
types.ALARM_NAMES = {
@ -480,7 +481,8 @@ types.ALARM_NAMES = {
"ReactorHighWaste",
"RPSTransient",
"RCSTransient",
"TurbineTrip"
"TurbineTrip",
"FacilityRadiation"
}
---@enum ALARM_PRIORITY

View File

@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
util.version = "1.5.1"
util.version = "1.5.2"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50

134
supervisor/alarm_ctl.lua Normal file
View File

@ -0,0 +1,134 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM_STATE = types.ALARM_STATE
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
local alarm_ctl = {}
alarm_ctl.AISTATE = AISTATE
alarm_ctl.AISTATE_NAMES = AISTATE_NAMES
-- update an alarm state given conditions
---@param caller_tag string tag to use in log messages
---@param alarm_states { [ALARM]: ALARM_STATE } unit instance
---@param tripped boolean if the alarm condition is sti ll active
---@param alarm alarm_def alarm table
---@param no_ring_back boolean? true to skip the ring back state, returning to inactive instead
---@return boolean new_trip if the alarm just changed to being tripped
function alarm_ctl.update_alarm_state(caller_tag, alarm_states, tripped, alarm, no_ring_back)
local int_state = alarm.state
local ext_state = alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
if no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c(caller_tag, " invalid alarm state for alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
end
return alarm_ctl

View File

@ -2,13 +2,19 @@ local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local unit = require("supervisor.unit")
local fac_update = require("supervisor.facility_update")
local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions")
local AISTATE = alarm_ctl.AISTATE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP
local PRIO = types.ALARM_PRIORITY
local PROCESS = types.PROCESS
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@ -138,7 +144,17 @@ function facility.new(config)
imtx_last_charge = 0,
imtx_last_charge_t = 0,
-- track faulted induction matrix update times to reject
imtx_faulted_times = { 0, 0, 0 }
imtx_faulted_times = { 0, 0, 0 },
-- facility alarms
---@type { [string]: alarm_def }
alarms = {
-- radiation monitor alarm for the facility
FacilityRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.FacilityRadiation, tier = PRIO.CRITICAL },
},
---@type { [ALARM]: ALARM_STATE }
alarm_states = {
[ALARM.FacilityRadiation] = ALARM_STATE.INACTIVE
}
}
--#region SETUP
@ -335,6 +351,9 @@ function facility.new(config)
-- unit tasks
f_update.unit_mgmt()
-- update alarm states right before updating the audio
f_update.update_alarms()
-- update alarm tones
f_update.alarm_audio()
end
@ -404,10 +423,14 @@ function facility.new(config)
end
end
-- ack all alarms on all reactor units
-- ack all alarms on all reactor units and the facility
function public.ack_all()
for i = 1, #self.units do
self.units[i].ack_all()
-- unit alarms
for i = 1, #self.units do self.units[i].ack_all() end
-- facility alarms
for id, state in pairs(self.alarm_states) do
if state == ALARM_STATE.TRIPPED then self.alarm_states[id] = ALARM_STATE.ACKED end
end
end

View File

@ -5,6 +5,8 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc")
local svsessions = require("supervisor.session.svsessions")
@ -714,11 +716,17 @@ function update.post_auto()
self.mode = next_mode
end
-- update facility alarm states
function update.update_alarms()
-- Facility Radiation
alarm_ctl.update_alarm_state("FAC", self.alarm_states, self.ascram_status.radiation, self.alarms.FacilityRadiation, true)
end
-- update alarm audio control
function update.alarm_audio()
local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false, false }
-- reset tone states before re-evaluting
for i = 1, #self.tone_states do self.tone_states[i] = false end
@ -734,8 +742,11 @@ function update.alarm_audio()
end
end
-- record facility alarms
alarms[ALARM.FacilityRadiation] = self.alarm_states[ALARM.FacilityRadiation] == ALARM_STATE.TRIPPED
-- clear testing alarms if we aren't using them
if not self.test_tone_reset then
-- clear testing alarms if we aren't using them
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
end
end
@ -774,7 +785,7 @@ function update.alarm_audio()
end
-- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then
if alarms[ALARM.ContainmentRadiation] or alarms[ALARM.FacilityRadiation] then
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one

View File

@ -23,7 +23,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.6.8"
local SUPERVISOR_VERSION = "v1.6.9"
local println = util.println
local println_ts = util.println_ts

View File

@ -3,20 +3,23 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local alarmctl = require("supervisor.alarm_ctl")
local logic = require("supervisor.unitlogic")
local plc = require("supervisor.session.plc")
local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions")
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local AISTATE = alarmctl.AISTATE
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL
local PRIO = types.ALARM_PRIORITY
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local TRI_FAIL = types.TRI_FAIL
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local PLC_S_CMDS = plc.PLC_S_CMDS
@ -37,23 +40,6 @@ local DT_KEYS = {
TurbinePower = "TPR"
}
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
-- burn rate to idle at
local IDLE_RATE = 0.01
@ -81,7 +67,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
num_boilers = num_boilers,
num_turbines = num_turbines,
aux_coolant = aux_coolant,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
types = { DT_KEYS = DT_KEYS },
-- rtus
rtu_list = {}, ---@type unit_session[][]
redstone = {}, ---@type redstone_session[]
@ -775,10 +761,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
-- acknowledge all alarms (if possible)
function public.ack_all()
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
for id, state in pairs(self.db.alarm_states) do
if state == ALARM_STATE.TRIPPED then self.db.alarm_states[id] = ALARM_STATE.ACKED end
end
end

View File

@ -1,12 +1,16 @@
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local const = require("scada-common.constants")
local log = require("scada-common.log")
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_ctl = require("supervisor.alarm_ctl")
local qtypes = require("supervisor.session.rtu.qtypes")
local plc = require("supervisor.session.plc")
local qtypes = require("supervisor.session.rtu.qtypes")
local AISTATE = alarm_ctl.AISTATE
local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
local TRI_FAIL = types.TRI_FAIL
@ -22,15 +26,6 @@ local IO = rsio.IO
local PLC_S_CMDS = plc.PLC_S_CMDS
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
@ -431,88 +426,7 @@ end
---@param alarm alarm_def alarm table
---@return boolean new_trip if the alarm just changed to being tripped
local function _update_alarm_state(self, tripped, alarm)
local AISTATE = self.types.AISTATE
local int_state = alarm.state
local ext_state = self.db.alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
else
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c("invalid alarm state for unit ", self.r_id, " alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
return alarm_ctl.update_alarm_state("UNIT " .. self.r_id, self.db.alarm_states, tripped, alarm)
end
-- evaluate alarm conditions
@ -632,8 +546,6 @@ end
---@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
@ -662,7 +574,6 @@ end
-- update the two unit status text messages
---@param self _unit_self unit instance
function logic.update_status_text(self)
local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator
-- check if an alarm is active (tripped or ack'd)
@ -826,7 +737,6 @@ end
-- handle unit redstone I/O
---@param self _unit_self unit instance
function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator
local cache = self.plc_cache
local rps = cache.rps_status
@ -906,7 +816,7 @@ function logic.handle_redstone(self)
if enable_emer_cool and not self.em_cool_opened then
log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<"))
log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]"))
log.debug(util.c("| ReactorOverTemp[", AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]"))
log.debug(util.c("| ReactorOverTemp[", alarm_ctl.AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]"))
for i = 1, #annunc.WaterLevelLow do
log.debug(util.c("| WaterLevelLow(", i, ")[", annunc.WaterLevelLow[i], "]"))