Merge branch 'devel' into 232-waste-valve-and-flow-monitoring-display
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
local audio = require("scada-common.audio")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
@@ -10,13 +11,17 @@ local qtypes = require("supervisor.session.rtu.qtypes")
|
||||
|
||||
local rsctl = require("supervisor.session.rsctl")
|
||||
|
||||
local TONE = audio.TONE
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
local PROCESS = types.PROCESS
|
||||
local PROCESS_NAMES = types.PROCESS_NAMES
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
|
||||
local IO = rsio.IO
|
||||
|
||||
@@ -65,6 +70,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
units = {},
|
||||
status_text = { "START UP", "initializing..." },
|
||||
all_sys_ok = false,
|
||||
allow_testing = false,
|
||||
-- rtus
|
||||
rtu_conn_count = 0,
|
||||
rtu_list = {},
|
||||
@@ -114,6 +120,12 @@ function facility.new(num_reactors, cooling_conf)
|
||||
waste_product = WASTE.PLUTONIUM,
|
||||
current_waste_product = WASTE.PLUTONIUM,
|
||||
pu_fallback = false,
|
||||
-- alarm tones
|
||||
tone_states = {},
|
||||
test_tone_set = false,
|
||||
test_tone_reset = false,
|
||||
test_tone_states = {},
|
||||
test_alarm_states = {},
|
||||
-- statistics
|
||||
im_stat_init = false,
|
||||
avg_charge = util.mov_avg(3, 0.0),
|
||||
@@ -133,6 +145,13 @@ function facility.new(num_reactors, cooling_conf)
|
||||
-- init redstone RTU I/O controller
|
||||
self.io_ctl = rsctl.new(self.redstone)
|
||||
|
||||
-- fill blank alarm/tone states
|
||||
for _ = 1, 12 do table.insert(self.test_alarm_states, false) end
|
||||
for _ = 1, 8 do
|
||||
table.insert(self.tone_states, false)
|
||||
table.insert(self.test_tone_states, false)
|
||||
end
|
||||
|
||||
-- check if all auto-controlled units completed ramping
|
||||
---@nodiscard
|
||||
local function _all_units_ramped()
|
||||
@@ -267,15 +286,20 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
-- supervisor sessions reporting the list of active RTU sessions
|
||||
---@param rtu_sessions table session list of all connected RTUs
|
||||
function public.report_rtus(rtu_sessions)
|
||||
self.rtu_conn_count = #rtu_sessions
|
||||
end
|
||||
function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end
|
||||
|
||||
-- update (iterate) the facility management
|
||||
function public.update()
|
||||
-- unlink RTU unit sessions if they are closed
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
||||
|
||||
-- check if test routines are allowed right now
|
||||
self.allow_testing = true
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
self.allow_testing = self.allow_testing and u.is_safe_idle()
|
||||
end
|
||||
|
||||
-- current state for process control
|
||||
local charge_update = 0
|
||||
local rate_update = 0
|
||||
@@ -778,6 +802,97 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Update Alarm Tones --
|
||||
------------------------
|
||||
|
||||
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 }
|
||||
|
||||
-- reset tone states before re-evaluting
|
||||
for i = 1, #self.tone_states do self.tone_states[i] = false end
|
||||
|
||||
if allow_test then
|
||||
alarms = self.test_alarm_states
|
||||
else
|
||||
-- check all alarms for all units
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
for id, alarm in pairs(u.get_alarms()) do
|
||||
alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- Evaluate Alarms --
|
||||
|
||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||
if alarms[ALARM.ContainmentBreach] then
|
||||
self.tone_states[TONE.T_1800Hz_Int_4Hz] = true
|
||||
else
|
||||
-- critical damage is highest priority CRITICAL level alarm
|
||||
if alarms[ALARM.CriticalDamage] then
|
||||
self.tone_states[TONE.T_660Hz_Int_125ms] = true
|
||||
else
|
||||
-- EMERGENCY level alarms + URGENT over temp
|
||||
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
||||
self.tone_states[TONE.T_544Hz_440Hz_Alt] = true
|
||||
-- URGENT level turbine trip
|
||||
elseif alarms[ALARM.TurbineTrip] then
|
||||
self.tone_states[TONE.T_745Hz_Int_1Hz] = true
|
||||
-- URGENT level reactor lost
|
||||
elseif alarms[ALARM.ReactorLost] then
|
||||
self.tone_states[TONE.T_340Hz_Int_2Hz] = true
|
||||
-- TIMELY level alarms
|
||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||
self.tone_states[TONE.T_800Hz_Int] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- check RPS transient URGENT level alarm
|
||||
if alarms[ALARM.RPSTransient] then
|
||||
self.tone_states[TONE.T_1000Hz_Int] = true
|
||||
-- disable really painful audio combination
|
||||
self.tone_states[TONE.T_340Hz_Int_2Hz] = false
|
||||
end
|
||||
end
|
||||
|
||||
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
||||
if alarms[ALARM.ContainmentRadiation] 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
|
||||
if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end
|
||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
||||
self.tone_states[TONE.T_745Hz_Int_1Hz] = false
|
||||
self.tone_states[TONE.T_800Hz_Int] = false
|
||||
self.tone_states[TONE.T_1000Hz_Int] = false
|
||||
end
|
||||
|
||||
-- add to tone states if testing is active
|
||||
if allow_test then
|
||||
for i = 1, #self.tone_states do
|
||||
self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i]
|
||||
end
|
||||
|
||||
self.test_tone_reset = false
|
||||
else
|
||||
if not self.test_tone_reset then
|
||||
-- clear testing tones if we aren't using them
|
||||
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
|
||||
end
|
||||
|
||||
-- flag that tones were reset
|
||||
self.test_tone_set = false
|
||||
self.test_tone_reset = true
|
||||
end
|
||||
end
|
||||
|
||||
-- call the update function of all units in the facility<br>
|
||||
@@ -919,8 +1034,52 @@ function facility.new(num_reactors, cooling_conf)
|
||||
return self.pu_fallback
|
||||
end
|
||||
|
||||
-- DIAGNOSTIC TESTING --
|
||||
|
||||
-- attempt to set a test tone state
|
||||
---@param id TONE|0 tone ID or 0 to disable all
|
||||
---@param state boolean state
|
||||
---@return boolean allow_testing, table test_tone_states
|
||||
function public.diag_set_test_tone(id, state)
|
||||
if self.allow_testing then
|
||||
self.test_tone_set = true
|
||||
self.test_tone_reset = false
|
||||
|
||||
if id == 0 then
|
||||
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
|
||||
else
|
||||
self.test_tone_states[id] = state
|
||||
end
|
||||
end
|
||||
|
||||
return self.allow_testing, self.test_tone_states
|
||||
end
|
||||
|
||||
-- attempt to set a test alarm state
|
||||
---@param id ALARM|0 alarm ID or 0 to disable all
|
||||
---@param state boolean state
|
||||
---@return boolean allow_testing, table test_alarm_states
|
||||
function public.diag_set_test_alarm(id, state)
|
||||
if self.allow_testing then
|
||||
self.test_tone_set = true
|
||||
self.test_tone_reset = false
|
||||
|
||||
if id == 0 then
|
||||
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
|
||||
else
|
||||
self.test_alarm_states[id] = state
|
||||
end
|
||||
end
|
||||
|
||||
return self.allow_testing, self.test_alarm_states
|
||||
end
|
||||
|
||||
-- READ STATES/PROPERTIES --
|
||||
|
||||
-- get current alarm tone on/off states
|
||||
---@nodiscard
|
||||
function public.get_alarm_tones() return self.tone_states end
|
||||
|
||||
-- get build properties of all facility devices
|
||||
---@nodiscard
|
||||
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
|
||||
|
||||
@@ -154,7 +154,8 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
local function _send_fac_status()
|
||||
local status = {
|
||||
facility.get_control_status(),
|
||||
facility.get_rtu_statuses()
|
||||
facility.get_rtu_statuses(),
|
||||
facility.get_alarm_tones()
|
||||
}
|
||||
|
||||
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
|
||||
|
||||
@@ -33,8 +33,9 @@ local PERIODICS = {
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
---@param facility facility facility data table
|
||||
---@param fp_ok boolean if the front panel UI is running
|
||||
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, fp_ok)
|
||||
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, fp_ok)
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||
|
||||
@@ -129,6 +130,55 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, fp_ok)
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then
|
||||
-- get the state of alarm tones
|
||||
_send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones())
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then
|
||||
local valid = false
|
||||
|
||||
-- attempt to set a tone state
|
||||
if pkt.scada_frame.is_authenticated() then
|
||||
if pkt.length == 2 then
|
||||
if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then
|
||||
valid = true
|
||||
|
||||
-- try to set tone states, then send back if testing is allowed
|
||||
local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2])
|
||||
_send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag tone set packet data type mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag tone set packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security")
|
||||
end
|
||||
|
||||
if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { false }) end
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then
|
||||
local valid = false
|
||||
|
||||
-- attempt to set an alarm state
|
||||
if pkt.scada_frame.is_authenticated() then
|
||||
if pkt.length == 2 then
|
||||
if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then
|
||||
valid = true
|
||||
|
||||
-- try to set alarm states, then send back if testing is allowed
|
||||
local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2])
|
||||
_send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag alarm set packet data type mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag alarm set packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security")
|
||||
end
|
||||
|
||||
if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { false }) end
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
|
||||
@@ -26,7 +26,8 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local PERIODICS = {
|
||||
KEEP_ALIVE = 2000
|
||||
KEEP_ALIVE = 2000,
|
||||
ALARM_TONES = 500
|
||||
}
|
||||
|
||||
-- create a new RTU session
|
||||
@@ -58,7 +59,8 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
||||
-- periodic messages
|
||||
periodics = {
|
||||
last_update = 0,
|
||||
keep_alive = 0
|
||||
keep_alive = 0,
|
||||
alarm_tones = 0
|
||||
},
|
||||
units = {}
|
||||
}
|
||||
@@ -389,6 +391,14 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
-- alarm tones
|
||||
|
||||
periodics.alarm_tones = periodics.alarm_tones + elapsed
|
||||
if periodics.alarm_tones >= PERIODICS.ALARM_TONES then
|
||||
_send_mgmt(SCADA_MGMT_TYPE.RTU_TONE_ALARM, { facility.get_alarm_tones() })
|
||||
periodics.alarm_tones = 0
|
||||
end
|
||||
|
||||
self.periodics.last_update = util.time()
|
||||
|
||||
--------------------------------------------
|
||||
|
||||
@@ -104,8 +104,8 @@ local function _sv_handle_outq(session)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning("[SVS] supervisor out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("[SVS] offending session: ", session))
|
||||
log.debug("[SVS] supervisor out queue handler exceeded 100ms queue process limit")
|
||||
log.debug(util.c("[SVS] offending session: ", session))
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -430,7 +430,8 @@ function svsessions.establish_pdg_session(source_addr, version)
|
||||
|
||||
local id = self.next_ids.pdg
|
||||
|
||||
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok)
|
||||
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.facility,
|
||||
self.fp_ok)
|
||||
table.insert(self.sessions.pdg, pdg_s)
|
||||
|
||||
local mt = {
|
||||
|
||||
@@ -733,6 +733,23 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
return false
|
||||
end
|
||||
|
||||
-- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active
|
||||
---@nodiscard
|
||||
function public.is_safe_idle()
|
||||
-- can't be disconnected
|
||||
if self.plc_i == nil then return false end
|
||||
|
||||
-- reactor must be stopped and RPS can't be tripped
|
||||
if self.plc_i.get_status().status or self.plc_i.get_db().rps_tripped then return false end
|
||||
|
||||
-- alarms must be inactive and not tripping
|
||||
for _, alarm in pairs(self.alarms) do
|
||||
if not (alarm.state == AISTATE.INACTIVE or alarm.state == AISTATE.RING_BACK) then return false end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- check if emergency coolant activation has been tripped
|
||||
---@nodiscard
|
||||
function public.is_emer_cool_tripped() return self.emcool_opened end
|
||||
|
||||
Reference in New Issue
Block a user