diff --git a/scada-common/types.lua b/scada-common/types.lua index 0d562a6..06f7c8d 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -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 diff --git a/scada-common/util.lua b/scada-common/util.lua index faa614c..2a2f81a 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -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 diff --git a/supervisor/alarm_ctl.lua b/supervisor/alarm_ctl.lua new file mode 100644 index 0000000..13ba237 --- /dev/null +++ b/supervisor/alarm_ctl.lua @@ -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 diff --git a/supervisor/facility.lua b/supervisor/facility.lua index f3cf18b..ca2369a 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -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 diff --git a/supervisor/facility_update.lua b/supervisor/facility_update.lua index a127f81..371700d 100644 --- a/supervisor/facility_update.lua +++ b/supervisor/facility_update.lua @@ -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 + -- 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 diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d1ee16d..fa0e1a6 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -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 diff --git a/supervisor/unit.lua b/supervisor/unit.lua index b448a95..6129d04 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -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 diff --git a/supervisor/unitlogic.lua b/supervisor/unit_logic.lua similarity index 88% rename from supervisor/unitlogic.lua rename to supervisor/unit_logic.lua index 6bd7470..db8af3a 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unit_logic.lua @@ -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,20 +26,10 @@ 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 ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS +local ALARM_LIMS = const.ALARM_LIMITS local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS - -local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS -local ALARM_LIMS = const.ALARM_LIMITS -local RS_THRESH = const.RS_THRESHOLDS +local RS_THRESH = const.RS_THRESHOLDS ---@class unit_logic_extension local logic = {} @@ -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], "]"))