#290 pocket page management and alarm test tool, supervisor pocket diagnostics system

This commit is contained in:
Mikayla Fischler
2023-07-29 18:16:59 -04:00
parent 775d4dc95b
commit df67795239
16 changed files with 858 additions and 250 deletions

View File

@@ -9,16 +9,16 @@ local unit = require("supervisor.unit")
local rsctl = require("supervisor.session.rsctl")
local TONES = audio.TONES
local TONES = audio.TONES
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES
local PRIO = types.ALARM_PRIORITY
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local WASTE = types.WASTE_PRODUCT
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local IO = rsio.IO
@@ -65,6 +65,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 = {},
@@ -115,7 +116,11 @@ function facility.new(num_reactors, cooling_conf)
current_waste_product = WASTE.PLUTONIUM,
pu_fallback = false,
-- alarm tones
tone_states = { false, false, false, false, false, false, false, false },
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),
@@ -135,6 +140,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()
@@ -269,15 +281,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
@@ -762,17 +779,43 @@ function facility.new(num_reactors, cooling_conf)
-- Update Alarm Tones --
------------------------
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
self.tone_states = { false, false, false, false, false, false, false, false}
local allow_test = self.allow_testing and self.test_tone_set
-- 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
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
for i = 1, #self.tone_states do
-- reset tone states before re-evaluting
self.tone_states[i] = false
-- clear testing tones if we aren't using them
if (not allow_test) and (not self.test_tone_reset) then self.test_tone_states[i] = false end
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)
-- clear testing alarms if we aren't using them
if not self.test_tone_reset then self.test_alarm_states[id] = false end
end
end
self.test_tone_reset = true
end
-- flag that tones were reset to notify diagnostic accessor
if not allow_test then
self.test_tone_set = false
self.test_tone_reset = true
end
-- Evaluate Alarms --
-- containment breach is worst case CRITICAL alarm, this takes priority
if alarms[ALARM.ContainmentBreach] then
self.tone_states[TONES.T_1800Hz_Int_4Hz] = true
@@ -815,6 +858,13 @@ function facility.new(num_reactors, cooling_conf)
self.tone_states[TONES.T_800Hz_Int] = false
self.tone_states[TONES.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
end
end
-- call the update function of all units in the facility<br>
@@ -956,6 +1006,46 @@ function facility.new(num_reactors, cooling_conf)
return self.pu_fallback
end
-- DIAGNOSTIC TESTING --
-- attempt to set a test tone state
---@param id tone_id|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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v0.21.0"
local SUPERVISOR_VERSION = "v0.22.0"
local println = util.println
local println_ts = util.println_ts

View File

@@ -719,6 +719,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
-- 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
-- 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
return true
end
-- get build properties of machines
--
-- filter options