Merge branch 'devel' into rtu-redstone-enhancements

This commit is contained in:
Mikayla Fischler 2025-05-03 09:54:29 -04:00
commit 07bb0f13e3
7 changed files with 222 additions and 156 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

137
supervisor/alarm_ctl.lua Normal file
View File

@ -0,0 +1,137 @@
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 based on its current status and if it is tripped
---@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
elseif 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
-- 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")
@ -643,7 +645,7 @@ function update.auto_safety()
end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault
local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.radiation or astatus.gen_fault
if scram and not self.ascram then
-- SCRAM all units
@ -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
if not self.test_tone_reset then
-- 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
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

@ -3,20 +3,23 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local logic = require("supervisor.unitlogic")
local alarm_ctl = require("supervisor.alarm_ctl")
local unit_logic = require("supervisor.unit_logic")
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 = alarm_ctl.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[]
@ -597,20 +583,20 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
_dt__compute_all()
-- update annunciator logic
logic.update_annunciator(self)
unit_logic.update_annunciator(self)
-- update alarm status
logic.update_alarms(self)
unit_logic.update_alarms(self)
-- if in auto mode, SCRAM on certain alarms
logic.update_auto_safety(public, self)
unit_logic.update_auto_safety(self, public)
-- update status text
logic.update_status_text(self)
unit_logic.update_status_text(self)
-- handle redstone I/O
if #self.redstone > 0 then
logic.handle_redstone(self)
unit_logic.handle_redstone(self)
elseif not self.plc_cache.rps_trip then
self.em_cool_opened = false
end
@ -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

@ -4,10 +4,14 @@ 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 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
local CONTAINER_MODE = types.CONTAINER_MODE
@ -22,19 +26,9 @@ 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
local ALARM_LIMS = const.ALARM_LIMITS
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local RS_THRESH = const.RS_THRESHOLDS
---@class unit_logic_extension
@ -426,97 +420,16 @@ function logic.update_annunciator(self)
end
-- update an alarm state given conditions
---@param self _unit_self unit instance
---@param self _unit_self
---@param tripped boolean if the alarm condition is still active
---@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
---@param self _unit_self unit instance
---@param self _unit_self
function logic.update_alarms(self)
local annunc = self.db.annunciator
local plc_cache = self.plc_cache
@ -629,11 +542,9 @@ function logic.update_alarms(self)
end
-- update the internal automatic safety control performed while in auto control mode
---@param self _unit_self
---@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
function logic.update_auto_safety(self, public)
if self.auto_engaged then
local alarmed = false
@ -660,9 +571,8 @@ function logic.update_auto_safety(public, self)
end
-- update the two unit status text messages
---@param self _unit_self unit instance
---@param self _unit_self
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)
@ -824,9 +734,8 @@ function logic.update_status_text(self)
end
-- handle unit redstone I/O
---@param self _unit_self unit instance
---@param self _unit_self
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 +815,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], "]"))