#280 moved alarm sounder logic to supervisor and tone control to common
This commit is contained in:
parent
7bd8f34773
commit
4192ea426c
@ -83,6 +83,8 @@ function iocontrol.init(conf, comms)
|
|||||||
scram_ack = __generic_ack,
|
scram_ack = __generic_ack,
|
||||||
ack_alarms_ack = __generic_ack,
|
ack_alarms_ack = __generic_ack,
|
||||||
|
|
||||||
|
alarm_tones = { false, false, false, false, false, false, false, false },
|
||||||
|
|
||||||
ps = psil.create(),
|
ps = psil.create(),
|
||||||
|
|
||||||
induction_ps_tbl = {},
|
induction_ps_tbl = {},
|
||||||
@ -664,6 +666,16 @@ function iocontrol.update_facility_status(status)
|
|||||||
end
|
end
|
||||||
|
|
||||||
fac.ps.publish("rtu_count", fac.rtu_count)
|
fac.ps.publish("rtu_count", fac.rtu_count)
|
||||||
|
|
||||||
|
-- alarm tone commands
|
||||||
|
|
||||||
|
if (type(status[3]) == "table") and (#status[3] == 8) then
|
||||||
|
fac.alarm_tones = status[3]
|
||||||
|
sounder.set(fac.alarm_tones)
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "alarm tones not a table or length mismatch")
|
||||||
|
valid = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
@ -1013,9 +1025,6 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
io.facility.ps.publish("sna_count", sna_count_sum)
|
io.facility.ps.publish("sna_count", sna_count_sum)
|
||||||
io.facility.ps.publish("pu_rate", pu_rate)
|
io.facility.ps.publish("pu_rate", pu_rate)
|
||||||
io.facility.ps.publish("po_rate", po_rate)
|
io.facility.ps.publish("po_rate", po_rate)
|
||||||
|
|
||||||
-- update alarm sounder
|
|
||||||
sounder.eval(io.units)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
|
|||||||
@ -2,269 +2,25 @@
|
|||||||
-- Alarm Sounder
|
-- Alarm Sounder
|
||||||
--
|
--
|
||||||
|
|
||||||
|
local audio = require("scada-common.audio")
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local types = require("scada-common.types")
|
|
||||||
local util = require("scada-common.util")
|
|
||||||
|
|
||||||
local ALARM = types.ALARM
|
|
||||||
local ALARM_STATE = types.ALARM_STATE
|
|
||||||
|
|
||||||
---@class sounder
|
---@class sounder
|
||||||
local sounder = {}
|
local sounder = {}
|
||||||
|
|
||||||
-- note: max samples = 0x20000 (128 * 1024 samples)
|
|
||||||
|
|
||||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
|
||||||
local _DRATE = 48000 -- 48kHz audio
|
|
||||||
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
|
||||||
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
|
||||||
|
|
||||||
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
|
||||||
|
|
||||||
local alarm_ctl = {
|
local alarm_ctl = {
|
||||||
speaker = nil,
|
speaker = nil,
|
||||||
volume = 0.5,
|
volume = 0.5,
|
||||||
playing = false,
|
stream = audio.new_stream()
|
||||||
num_active = 0,
|
|
||||||
next_block = 1,
|
|
||||||
-- split audio up into 0.5s samples so specific components can be ended quicker
|
|
||||||
quad_buffer = { {}, {}, {}, {} }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
|
||||||
|
|
||||||
local T_340Hz_Int_2Hz = 1
|
|
||||||
local T_544Hz_440Hz_Alt = 2
|
|
||||||
local T_660Hz_Int_125ms = 3
|
|
||||||
local T_745Hz_Int_1Hz = 4
|
|
||||||
local T_800Hz_Int = 5
|
|
||||||
local T_800Hz_1000Hz_Alt = 6
|
|
||||||
local T_1000Hz_Int = 7
|
|
||||||
local T_1800Hz_Int_4Hz = 8
|
|
||||||
|
|
||||||
local TONES = {
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 340Hz @ 2Hz Intermittent
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 660Hz @ 125ms On 125ms Off
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 745Hz @ 1Hz Intermittent
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800Hz @ 0.25s On 1.75s Off
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800/1000Hz @ 0.25s Alternating
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 1KHz 1s on, 1s off Intermittent
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } } -- 1.8KHz @ 4Hz Intermittent
|
|
||||||
}
|
|
||||||
|
|
||||||
-- calculate how many samples are in the given number of milliseconds
|
|
||||||
---@nodiscard
|
|
||||||
---@param ms integer milliseconds
|
|
||||||
---@return integer samples
|
|
||||||
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
|
||||||
|
|
||||||
--#region Tone Generation (the Maths)
|
|
||||||
|
|
||||||
-- 340Hz @ 2Hz Intermittent
|
|
||||||
local function gen_tone_1()
|
|
||||||
local t, dt = 0, _2_PI * 340 / _DRATE
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[1].component[1][i] = val
|
|
||||||
TONES[1].component[3][i] = val
|
|
||||||
TONES[1].component[2][i] = 0
|
|
||||||
TONES[1].component[4][i] = 0
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 544Hz 100mS / 440Hz 400mS Alternating
|
|
||||||
local function gen_tone_2()
|
|
||||||
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
|
||||||
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
|
||||||
local alternate_at = ms_to_samples(100)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local value
|
|
||||||
|
|
||||||
if i <= alternate_at then
|
|
||||||
value = math.floor(math.sin(t1) * _MAX_VAL)
|
|
||||||
t1 = (t1 + dt1) % _2_PI
|
|
||||||
else
|
|
||||||
value = math.floor(math.sin(t2) * _MAX_VAL)
|
|
||||||
t2 = (t2 + dt2) % _2_PI
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[2].component[1][i] = value
|
|
||||||
TONES[2].component[2][i] = value
|
|
||||||
TONES[2].component[3][i] = value
|
|
||||||
TONES[2].component[4][i] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 660Hz @ 125ms On 125ms Off
|
|
||||||
local function gen_tone_3()
|
|
||||||
local elapsed_samples = 0
|
|
||||||
local alternate_after = ms_to_samples(125)
|
|
||||||
local alternate_at = alternate_after
|
|
||||||
local mode = true
|
|
||||||
|
|
||||||
local t, dt = 0, _2_PI * 660 / _DRATE
|
|
||||||
|
|
||||||
for set = 1, 4 do
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
if mode then
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[3].component[set][i] = val
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
else
|
|
||||||
t = 0
|
|
||||||
TONES[3].component[set][i] = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if elapsed_samples == alternate_at then
|
|
||||||
mode = not mode
|
|
||||||
alternate_at = elapsed_samples + alternate_after
|
|
||||||
end
|
|
||||||
|
|
||||||
elapsed_samples = elapsed_samples + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 745Hz @ 1Hz Intermittent
|
|
||||||
local function gen_tone_4()
|
|
||||||
local t, dt = 0, _2_PI * 745 / _DRATE
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[4].component[1][i] = val
|
|
||||||
TONES[4].component[3][i] = val
|
|
||||||
TONES[4].component[2][i] = 0
|
|
||||||
TONES[4].component[4][i] = 0
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 800Hz @ 0.25s On 1.75s Off
|
|
||||||
local function gen_tone_5()
|
|
||||||
local t, dt = 0, _2_PI * 800 / _DRATE
|
|
||||||
local stop_at = ms_to_samples(250)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
|
|
||||||
if i > stop_at then
|
|
||||||
TONES[5].component[1][i] = val
|
|
||||||
else
|
|
||||||
TONES[5].component[1][i] = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[5].component[2][i] = 0
|
|
||||||
TONES[5].component[3][i] = 0
|
|
||||||
TONES[5].component[4][i] = 0
|
|
||||||
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 1000/800Hz @ 0.25s Alternating
|
|
||||||
local function gen_tone_6()
|
|
||||||
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
|
||||||
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
|
||||||
|
|
||||||
local alternate_at = ms_to_samples(250)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val
|
|
||||||
if i <= alternate_at then
|
|
||||||
val = math.floor(math.sin(t1) * _MAX_VAL)
|
|
||||||
t1 = (t1 + dt1) % _2_PI
|
|
||||||
else
|
|
||||||
val = math.floor(math.sin(t2) * _MAX_VAL)
|
|
||||||
t2 = (t2 + dt2) % _2_PI
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[6].component[1][i] = val
|
|
||||||
TONES[6].component[2][i] = val
|
|
||||||
TONES[6].component[3][i] = val
|
|
||||||
TONES[6].component[4][i] = val
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 1KHz 1s on, 1s off Intermittent
|
|
||||||
local function gen_tone_7()
|
|
||||||
local t, dt = 0, _2_PI * 1000 / _DRATE
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[7].component[1][i] = val
|
|
||||||
TONES[7].component[2][i] = val
|
|
||||||
TONES[7].component[3][i] = 0
|
|
||||||
TONES[7].component[4][i] = 0
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 1800Hz @ 4Hz Intermittent
|
|
||||||
local function gen_tone_8()
|
|
||||||
local t, dt = 0, _2_PI * 1800 / _DRATE
|
|
||||||
|
|
||||||
local off_at = ms_to_samples(250)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = 0
|
|
||||||
|
|
||||||
if i <= off_at then
|
|
||||||
val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[8].component[1][i] = val
|
|
||||||
TONES[8].component[2][i] = val
|
|
||||||
TONES[8].component[3][i] = val
|
|
||||||
TONES[8].component[4][i] = val
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--#endregion
|
|
||||||
|
|
||||||
-- hard audio limiter
|
|
||||||
---@nodiscard
|
|
||||||
---@param output number output level
|
|
||||||
---@return number limited -128.0 to 127.0
|
|
||||||
local function limit(output)
|
|
||||||
return math.max(-128, math.min(127, output))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- zero the alarm audio buffer
|
|
||||||
local function zero()
|
|
||||||
for i = 1, 4 do
|
|
||||||
for s = 1, _05s_SAMPLES do alarm_ctl.quad_buffer[i][s] = 0 end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- add an alarm to the output buffer
|
|
||||||
---@param alarm_idx integer tone ID
|
|
||||||
local function add(alarm_idx)
|
|
||||||
alarm_ctl.num_active = alarm_ctl.num_active + 1
|
|
||||||
TONES[alarm_idx].active = true
|
|
||||||
|
|
||||||
for i = 1, 4 do
|
|
||||||
for s = 1, _05s_SAMPLES do
|
|
||||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] + TONES[alarm_idx].component[i][s])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- start audio or continue audio on buffer empty
|
-- start audio or continue audio on buffer empty
|
||||||
---@return boolean success successfully added buffer to audio output
|
---@return boolean success successfully added buffer to audio output
|
||||||
local function play()
|
local function play()
|
||||||
if not alarm_ctl.playing then
|
if not alarm_ctl.playing then
|
||||||
alarm_ctl.playing = true
|
alarm_ctl.playing = true
|
||||||
alarm_ctl.next_block = 1
|
|
||||||
|
|
||||||
return sounder.continue()
|
return sounder.continue()
|
||||||
else
|
else return true end
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- initialize the annunciator alarm system
|
-- initialize the annunciator alarm system
|
||||||
@ -273,23 +29,10 @@ end
|
|||||||
function sounder.init(speaker, volume)
|
function sounder.init(speaker, volume)
|
||||||
alarm_ctl.speaker = speaker
|
alarm_ctl.speaker = speaker
|
||||||
alarm_ctl.speaker.stop()
|
alarm_ctl.speaker.stop()
|
||||||
|
|
||||||
alarm_ctl.volume = volume
|
alarm_ctl.volume = volume
|
||||||
alarm_ctl.playing = false
|
alarm_ctl.stream.stop()
|
||||||
alarm_ctl.num_active = 0
|
|
||||||
alarm_ctl.next_block = 1
|
|
||||||
|
|
||||||
zero()
|
audio.generate_tones()
|
||||||
|
|
||||||
-- generate tones
|
|
||||||
gen_tone_1()
|
|
||||||
gen_tone_2()
|
|
||||||
gen_tone_3()
|
|
||||||
gen_tone_4()
|
|
||||||
gen_tone_5()
|
|
||||||
gen_tone_6()
|
|
||||||
gen_tone_7()
|
|
||||||
gen_tone_8()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- reconnect the speaker peripheral
|
-- reconnect the speaker peripheral
|
||||||
@ -297,172 +40,65 @@ end
|
|||||||
function sounder.reconnect(speaker)
|
function sounder.reconnect(speaker)
|
||||||
alarm_ctl.speaker = speaker
|
alarm_ctl.speaker = speaker
|
||||||
alarm_ctl.playing = false
|
alarm_ctl.playing = false
|
||||||
alarm_ctl.next_block = 1
|
alarm_ctl.stream.stop()
|
||||||
alarm_ctl.num_active = 0
|
|
||||||
for id = 1, #TONES do TONES[id].active = false end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check alarm state to enable/disable alarms
|
-- set alarm tones
|
||||||
---@param units table|nil unit list or nil to use test mode
|
---@param states table alarm tone commands from supervisor
|
||||||
function sounder.eval(units)
|
function sounder.set(states)
|
||||||
local changed = false
|
-- set tone states
|
||||||
local any_active = false
|
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
|
||||||
local new_states = { false, false, false, false, false, false, false, false }
|
|
||||||
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
|
||||||
|
|
||||||
if units ~= nil then
|
-- re-compute output if needed, then play audio if available
|
||||||
-- check all alarms for all units
|
if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
|
||||||
for i = 1, #units do
|
if alarm_ctl.stream.has_next_block() then play() else sounder.stop() end
|
||||||
local unit = units[i] ---@type ioctl_unit
|
|
||||||
for id = 1, #unit.alarms do
|
|
||||||
alarms[id] = alarms[id] or (unit.alarms[id] == ALARM_STATE.TRIPPED)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
alarms = test_alarms
|
|
||||||
end
|
|
||||||
|
|
||||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
|
||||||
if alarms[ALARM.ContainmentBreach] then
|
|
||||||
new_states[T_1800Hz_Int_4Hz] = true
|
|
||||||
else
|
|
||||||
-- critical damage is highest priority CRITICAL level alarm
|
|
||||||
if alarms[ALARM.CriticalDamage] then
|
|
||||||
new_states[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
|
|
||||||
new_states[T_544Hz_440Hz_Alt] = true
|
|
||||||
-- URGENT level turbine trip
|
|
||||||
elseif alarms[ALARM.TurbineTrip] then
|
|
||||||
new_states[T_745Hz_Int_1Hz] = true
|
|
||||||
-- URGENT level reactor lost
|
|
||||||
elseif alarms[ALARM.ReactorLost] then
|
|
||||||
new_states[T_340Hz_Int_2Hz] = true
|
|
||||||
-- TIMELY level alarms
|
|
||||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
|
||||||
new_states[T_800Hz_Int] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check RPS transient URGENT level alarm
|
|
||||||
if alarms[ALARM.RPSTransient] then
|
|
||||||
new_states[T_1000Hz_Int] = true
|
|
||||||
-- disable really painful audio combination
|
|
||||||
new_states[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
|
|
||||||
new_states[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 new_states[T_1000Hz_Int] and alarms[ALARM.ReactorLost] then new_states[T_340Hz_Int_2Hz] = true end
|
|
||||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
|
||||||
new_states[T_745Hz_Int_1Hz] = false
|
|
||||||
new_states[T_800Hz_Int] = false
|
|
||||||
new_states[T_1000Hz_Int] = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if any changed, check if any active, update active flags
|
|
||||||
for id = 1, #TONES do
|
|
||||||
if new_states[id] ~= TONES[id].active then
|
|
||||||
TONES[id].active = new_states[id]
|
|
||||||
changed = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if TONES[id].active then any_active = true end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- zero and re-add tones if changed
|
|
||||||
if changed then
|
|
||||||
zero()
|
|
||||||
|
|
||||||
for id = 1, #TONES do
|
|
||||||
if TONES[id].active then add(id) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if any_active then play() else sounder.stop() end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- stop all audio and clear output buffer
|
-- stop all audio and clear output buffer
|
||||||
function sounder.stop()
|
function sounder.stop()
|
||||||
alarm_ctl.playing = false
|
alarm_ctl.playing = false
|
||||||
alarm_ctl.speaker.stop()
|
alarm_ctl.speaker.stop()
|
||||||
alarm_ctl.next_block = 1
|
alarm_ctl.stream.stop()
|
||||||
alarm_ctl.num_active = 0
|
|
||||||
for id = 1, #TONES do TONES[id].active = false end
|
|
||||||
zero()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- continue audio on buffer empty
|
-- continue audio on buffer empty
|
||||||
---@return boolean success successfully added buffer to audio output
|
---@return boolean success successfully added buffer to audio output
|
||||||
function sounder.continue()
|
function sounder.continue()
|
||||||
|
local success = false
|
||||||
|
|
||||||
if alarm_ctl.playing then
|
if alarm_ctl.playing then
|
||||||
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
|
if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
|
||||||
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
|
success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
|
||||||
|
if not success then log.error("SOUNDER: error playing audio") end
|
||||||
alarm_ctl.next_block = alarm_ctl.next_block + 1
|
|
||||||
if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
log.debug("SOUNDER: error playing audio")
|
|
||||||
end
|
|
||||||
|
|
||||||
return success
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return success
|
||||||
end
|
end
|
||||||
|
|
||||||
--#region Test Functions
|
--#region Test Functions
|
||||||
|
|
||||||
function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
|
-- function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
|
||||||
function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
|
-- function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
|
||||||
function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
|
-- function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
|
||||||
function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
|
-- function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
|
||||||
function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
|
-- function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
|
||||||
function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
|
-- function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
|
||||||
function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
|
-- function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
|
||||||
function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
|
-- function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
|
||||||
|
|
||||||
function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
|
-- function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
|
||||||
function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
|
-- function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
|
||||||
function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
|
-- function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
|
||||||
function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
|
-- function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
|
||||||
function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
|
-- function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
|
||||||
function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
|
-- function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
|
||||||
function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
|
-- function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
|
||||||
function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
|
-- function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
|
||||||
function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
|
-- function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
|
||||||
function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
|
-- function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
|
||||||
function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
|
-- function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
|
||||||
function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
|
-- function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
|
||||||
|
|
||||||
-- power rescaling limiter test
|
|
||||||
function sounder.test_power_scale()
|
|
||||||
local start = util.time_ms()
|
|
||||||
|
|
||||||
zero()
|
|
||||||
|
|
||||||
for id = 1, #TONES do
|
|
||||||
if TONES[id].active then
|
|
||||||
for i = 1, 4 do
|
|
||||||
for s = 1, _05s_SAMPLES do
|
|
||||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] +
|
|
||||||
(TONES[id].component[i][s] / math.sqrt(alarm_ctl.num_active)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
|
|
||||||
end
|
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder")
|
|||||||
|
|
||||||
local apisessions = require("coordinator.session.apisessions")
|
local apisessions = require("coordinator.session.apisessions")
|
||||||
|
|
||||||
local COORDINATOR_VERSION = "v0.21.2"
|
local COORDINATOR_VERSION = "v0.22.0"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|||||||
313
scada-common/audio.lua
Normal file
313
scada-common/audio.lua
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
--
|
||||||
|
-- Audio & Tone Control for Alarms
|
||||||
|
--
|
||||||
|
|
||||||
|
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
||||||
|
|
||||||
|
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||||
|
|
||||||
|
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||||
|
local _DRATE = 48000 -- 48kHz audio
|
||||||
|
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
||||||
|
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
||||||
|
|
||||||
|
---@class audio
|
||||||
|
local audio = {}
|
||||||
|
|
||||||
|
---@enum tone_id
|
||||||
|
local TONES = {
|
||||||
|
T_340Hz_Int_2Hz = 1,
|
||||||
|
T_544Hz_440Hz_Alt = 2,
|
||||||
|
T_660Hz_Int_125ms = 3,
|
||||||
|
T_745Hz_Int_1Hz = 4,
|
||||||
|
T_800Hz_Int = 5,
|
||||||
|
T_800Hz_1000Hz_Alt = 6,
|
||||||
|
T_1000Hz_Int = 7,
|
||||||
|
T_1800Hz_Int_4Hz = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.TONES = TONES
|
||||||
|
|
||||||
|
local tone_data = {
|
||||||
|
{ {}, {}, {}, {} }, -- 340Hz @ 2Hz Intermittent
|
||||||
|
{ {}, {}, {}, {} }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
||||||
|
{ {}, {}, {}, {} }, -- 660Hz @ 125ms On 125ms Off
|
||||||
|
{ {}, {}, {}, {} }, -- 745Hz @ 1Hz Intermittent
|
||||||
|
{ {}, {}, {}, {} }, -- 800Hz @ 0.25s On 1.75s Off
|
||||||
|
{ {}, {}, {}, {} }, -- 800/1000Hz @ 0.25s Alternating
|
||||||
|
{ {}, {}, {}, {} }, -- 1KHz 1s on, 1s off Intermittent
|
||||||
|
{ {}, {}, {}, {} } -- 1.8KHz @ 4Hz Intermittent
|
||||||
|
}
|
||||||
|
|
||||||
|
-- calculate how many samples are in the given number of milliseconds
|
||||||
|
---@nodiscard
|
||||||
|
---@param ms integer milliseconds
|
||||||
|
---@return integer samples
|
||||||
|
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
||||||
|
|
||||||
|
--#region Tone Generation (the Maths)
|
||||||
|
|
||||||
|
-- 340Hz @ 2Hz Intermittent
|
||||||
|
local function gen_tone_1()
|
||||||
|
local t, dt = 0, _2_PI * 340 / _DRATE
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[1][1][i] = val
|
||||||
|
tone_data[1][3][i] = val
|
||||||
|
tone_data[1][2][i] = 0
|
||||||
|
tone_data[1][4][i] = 0
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 544Hz 100mS / 440Hz 400mS Alternating
|
||||||
|
local function gen_tone_2()
|
||||||
|
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
||||||
|
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
||||||
|
local alternate_at = ms_to_samples(100)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local value
|
||||||
|
|
||||||
|
if i <= alternate_at then
|
||||||
|
value = math.floor(math.sin(t1) * _MAX_VAL)
|
||||||
|
t1 = (t1 + dt1) % _2_PI
|
||||||
|
else
|
||||||
|
value = math.floor(math.sin(t2) * _MAX_VAL)
|
||||||
|
t2 = (t2 + dt2) % _2_PI
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[2][1][i] = value
|
||||||
|
tone_data[2][2][i] = value
|
||||||
|
tone_data[2][3][i] = value
|
||||||
|
tone_data[2][4][i] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 660Hz @ 125ms On 125ms Off
|
||||||
|
local function gen_tone_3()
|
||||||
|
local elapsed_samples = 0
|
||||||
|
local alternate_after = ms_to_samples(125)
|
||||||
|
local alternate_at = alternate_after
|
||||||
|
local mode = true
|
||||||
|
|
||||||
|
local t, dt = 0, _2_PI * 660 / _DRATE
|
||||||
|
|
||||||
|
for set = 1, 4 do
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
if mode then
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[3][set][i] = val
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
else
|
||||||
|
t = 0
|
||||||
|
tone_data[3][set][i] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if elapsed_samples == alternate_at then
|
||||||
|
mode = not mode
|
||||||
|
alternate_at = elapsed_samples + alternate_after
|
||||||
|
end
|
||||||
|
|
||||||
|
elapsed_samples = elapsed_samples + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 745Hz @ 1Hz Intermittent
|
||||||
|
local function gen_tone_4()
|
||||||
|
local t, dt = 0, _2_PI * 745 / _DRATE
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[4][1][i] = val
|
||||||
|
tone_data[4][3][i] = val
|
||||||
|
tone_data[4][2][i] = 0
|
||||||
|
tone_data[4][4][i] = 0
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 800Hz @ 0.25s On 1.75s Off
|
||||||
|
local function gen_tone_5()
|
||||||
|
local t, dt = 0, _2_PI * 800 / _DRATE
|
||||||
|
local stop_at = ms_to_samples(250)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
|
||||||
|
if i > stop_at then
|
||||||
|
tone_data[5][1][i] = val
|
||||||
|
else
|
||||||
|
tone_data[5][1][i] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[5][2][i] = 0
|
||||||
|
tone_data[5][3][i] = 0
|
||||||
|
tone_data[5][4][i] = 0
|
||||||
|
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1000/800Hz @ 0.25s Alternating
|
||||||
|
local function gen_tone_6()
|
||||||
|
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
||||||
|
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
||||||
|
|
||||||
|
local alternate_at = ms_to_samples(250)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val
|
||||||
|
if i <= alternate_at then
|
||||||
|
val = math.floor(math.sin(t1) * _MAX_VAL)
|
||||||
|
t1 = (t1 + dt1) % _2_PI
|
||||||
|
else
|
||||||
|
val = math.floor(math.sin(t2) * _MAX_VAL)
|
||||||
|
t2 = (t2 + dt2) % _2_PI
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[6][1][i] = val
|
||||||
|
tone_data[6][2][i] = val
|
||||||
|
tone_data[6][3][i] = val
|
||||||
|
tone_data[6][4][i] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1KHz 1s on, 1s off Intermittent
|
||||||
|
local function gen_tone_7()
|
||||||
|
local t, dt = 0, _2_PI * 1000 / _DRATE
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[7][1][i] = val
|
||||||
|
tone_data[7][2][i] = val
|
||||||
|
tone_data[7][3][i] = 0
|
||||||
|
tone_data[7][4][i] = 0
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1800Hz @ 4Hz Intermittent
|
||||||
|
local function gen_tone_8()
|
||||||
|
local t, dt = 0, _2_PI * 1800 / _DRATE
|
||||||
|
|
||||||
|
local off_at = ms_to_samples(250)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = 0
|
||||||
|
|
||||||
|
if i <= off_at then
|
||||||
|
val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[8][1][i] = val
|
||||||
|
tone_data[8][2][i] = val
|
||||||
|
tone_data[8][3][i] = val
|
||||||
|
tone_data[8][4][i] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--#endregion
|
||||||
|
|
||||||
|
-- generate all 8 tone sequences
|
||||||
|
function audio.generate_tones()
|
||||||
|
gen_tone_1(); gen_tone_2(); gen_tone_3(); gen_tone_4(); gen_tone_5(); gen_tone_6(); gen_tone_7(); gen_tone_8()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- hard audio limiter
|
||||||
|
---@nodiscard
|
||||||
|
---@param output number output level
|
||||||
|
---@return number limited -128.0 to 127.0
|
||||||
|
local function limit(output)
|
||||||
|
return math.max(-128, math.min(127, output))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clear output buffer
|
||||||
|
---@param buffer table quad buffer
|
||||||
|
local function clear(buffer)
|
||||||
|
for i = 1, 4 do
|
||||||
|
for s = 1, _05s_SAMPLES do buffer[i][s] = 0 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create a new audio tone stream controller
|
||||||
|
function audio.new_stream()
|
||||||
|
local self = {
|
||||||
|
any_active = false,
|
||||||
|
need_recompute = false,
|
||||||
|
next_block = 1,
|
||||||
|
-- split audio up into 0.5s samples, so specific components can be ended quicker
|
||||||
|
quad_buffer = { {}, {}, {}, {} },
|
||||||
|
-- all tone enable states
|
||||||
|
tone_active = { false, false, false, false, false, false, false, false }
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(self.quad_buffer)
|
||||||
|
|
||||||
|
---@class tone_stream
|
||||||
|
local public = {}
|
||||||
|
|
||||||
|
-- add a tone to the output buffer
|
||||||
|
---@param index tone_id tone ID
|
||||||
|
---@param active boolean active state
|
||||||
|
function public.set_active(index, active)
|
||||||
|
if self.tone_active[index] then
|
||||||
|
if self.tone_active[index] ~= active then self.need_recompute = true end
|
||||||
|
self.tone_active[index] = active
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if a tone is active
|
||||||
|
---@param index tone_id tone index
|
||||||
|
function public.is_active(index)
|
||||||
|
if self.tone_active[index] then return self.tone_active[index] end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set all tones inactive, reset next block, and clear output buffer
|
||||||
|
function public.stop()
|
||||||
|
for i = 1, #self.tone_active do self.tone_active[i] = false end
|
||||||
|
self.next_block = 1
|
||||||
|
clear(self.quad_buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if the output buffer needs to be recomputed due to changes
|
||||||
|
function public.is_recompute_needed() return self.need_recompute end
|
||||||
|
|
||||||
|
-- re-compute the output buffer
|
||||||
|
function public.compute_buffer()
|
||||||
|
clear(self.quad_buffer)
|
||||||
|
|
||||||
|
self.need_recompute = false
|
||||||
|
self.any_active = false
|
||||||
|
|
||||||
|
for id = 1, #tone_data do
|
||||||
|
if self.tone_active[id] then
|
||||||
|
self.any_active = true
|
||||||
|
for i = 1, 4 do
|
||||||
|
local buffer = self.quad_buffer[i]
|
||||||
|
local values = tone_data[id]
|
||||||
|
for s = 1, _05s_SAMPLES do self.quad_buffer[i][s] = limit(buffer[s] + values[s]) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if the next audio block has data
|
||||||
|
function public.has_next_block() return #self.quad_buffer[self.next_block] > 0 end
|
||||||
|
|
||||||
|
-- get the next audio block
|
||||||
|
function public.get_next_block()
|
||||||
|
local block = self.quad_buffer[self.next_block]
|
||||||
|
self.next_block = self.next_block + 1
|
||||||
|
if self.next_block > 4 then self.next_block = 1 end
|
||||||
|
return block
|
||||||
|
end
|
||||||
|
|
||||||
|
return public
|
||||||
|
end
|
||||||
|
|
||||||
|
return audio
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
local audio = require("scada-common.audio")
|
||||||
local const = require("scada-common.constants")
|
local const = require("scada-common.constants")
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local rsio = require("scada-common.rsio")
|
local rsio = require("scada-common.rsio")
|
||||||
@ -8,12 +9,16 @@ local unit = require("supervisor.unit")
|
|||||||
|
|
||||||
local rsctl = require("supervisor.session.rsctl")
|
local rsctl = require("supervisor.session.rsctl")
|
||||||
|
|
||||||
local PROCESS = types.PROCESS
|
local TONES = audio.TONES
|
||||||
|
|
||||||
|
local PROCESS = types.PROCESS
|
||||||
local PROCESS_NAMES = types.PROCESS_NAMES
|
local PROCESS_NAMES = types.PROCESS_NAMES
|
||||||
local PRIO = types.ALARM_PRIORITY
|
local PRIO = types.ALARM_PRIORITY
|
||||||
|
local ALARM = types.ALARM
|
||||||
|
local ALARM_STATE = types.ALARM_STATE
|
||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
local WASTE = types.WASTE_PRODUCT
|
local WASTE = types.WASTE_PRODUCT
|
||||||
local WASTE_MODE = types.WASTE_MODE
|
local WASTE_MODE = types.WASTE_MODE
|
||||||
|
|
||||||
local IO = rsio.IO
|
local IO = rsio.IO
|
||||||
|
|
||||||
@ -109,6 +114,8 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
waste_product = WASTE.PLUTONIUM,
|
waste_product = WASTE.PLUTONIUM,
|
||||||
current_waste_product = WASTE.PLUTONIUM,
|
current_waste_product = WASTE.PLUTONIUM,
|
||||||
pu_fallback = false,
|
pu_fallback = false,
|
||||||
|
-- alarm tones
|
||||||
|
tone_states = { false, false, false, false, false, false, false, false },
|
||||||
-- statistics
|
-- statistics
|
||||||
im_stat_init = false,
|
im_stat_init = false,
|
||||||
avg_charge = util.mov_avg(3, 0.0),
|
avg_charge = util.mov_avg(3, 0.0),
|
||||||
@ -750,6 +757,63 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
|
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
|
||||||
self.current_waste_product = WASTE.PLUTONIUM
|
self.current_waste_product = WASTE.PLUTONIUM
|
||||||
else self.current_waste_product = self.waste_product end
|
else self.current_waste_product = self.waste_product end
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
-- Update Alarm Tones --
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||||
|
if alarms[ALARM.ContainmentBreach] then
|
||||||
|
self.tone_states[TONES.T_1800Hz_Int_4Hz] = true
|
||||||
|
else
|
||||||
|
-- critical damage is highest priority CRITICAL level alarm
|
||||||
|
if alarms[ALARM.CriticalDamage] then
|
||||||
|
self.tone_states[TONES.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[TONES.T_544Hz_440Hz_Alt] = true
|
||||||
|
-- URGENT level turbine trip
|
||||||
|
elseif alarms[ALARM.TurbineTrip] then
|
||||||
|
self.tone_states[TONES.T_745Hz_Int_1Hz] = true
|
||||||
|
-- URGENT level reactor lost
|
||||||
|
elseif alarms[ALARM.ReactorLost] then
|
||||||
|
self.tone_states[TONES.T_340Hz_Int_2Hz] = true
|
||||||
|
-- TIMELY level alarms
|
||||||
|
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||||
|
self.tone_states[TONES.T_800Hz_Int] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check RPS transient URGENT level alarm
|
||||||
|
if alarms[ALARM.RPSTransient] then
|
||||||
|
self.tone_states[TONES.T_1000Hz_Int] = true
|
||||||
|
-- disable really painful audio combination
|
||||||
|
self.tone_states[TONES.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[TONES.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[TONES.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONES.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[TONES.T_745Hz_Int_1Hz] = false
|
||||||
|
self.tone_states[TONES.T_800Hz_Int] = false
|
||||||
|
self.tone_states[TONES.T_1000Hz_Int] = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- call the update function of all units in the facility<br>
|
-- call the update function of all units in the facility<br>
|
||||||
@ -893,6 +957,10 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
|
|
||||||
-- READ STATES/PROPERTIES --
|
-- 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
|
-- get build properties of all facility devices
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
|
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
|
||||||
|
|||||||
@ -150,7 +150,8 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
|||||||
local function _send_fac_status()
|
local function _send_fac_status()
|
||||||
local status = {
|
local status = {
|
||||||
facility.get_control_status(),
|
facility.get_control_status(),
|
||||||
facility.get_rtu_statuses()
|
facility.get_rtu_statuses(),
|
||||||
|
facility.get_alarm_tones()
|
||||||
}
|
}
|
||||||
|
|
||||||
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
|
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
|
||||||
|
|||||||
@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
|
|||||||
|
|
||||||
local svsessions = require("supervisor.session.svsessions")
|
local svsessions = require("supervisor.session.svsessions")
|
||||||
|
|
||||||
local SUPERVISOR_VERSION = "v0.20.4"
|
local SUPERVISOR_VERSION = "v0.21.0"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user