#77 alarm sounder

This commit is contained in:
Mikayla Fischler 2022-12-04 13:59:10 -05:00
parent 518ee8272a
commit 4030fdc5c9
9 changed files with 575 additions and 25 deletions

View File

@ -1,11 +1,15 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local psil = require("scada-common.psil") local psil = require("scada-common.psil")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local sounder = require("coordinator.sounder")
local CRDN_COMMANDS = comms.CRDN_COMMANDS local CRDN_COMMANDS = comms.CRDN_COMMANDS
local ALARM_STATE = types.ALARM_STATE
local iocontrol = {} local iocontrol = {}
---@class ioctl ---@class ioctl
@ -74,7 +78,21 @@ function iocontrol.init(conf, comms)
t_trip = { ack = function () ack(12) end, reset = function () reset(12) end } t_trip = { ack = function () ack(12) end, reset = function () reset(12) end }
}, },
alarms = {}, ---@type alarms ---@type alarms
alarms = {
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE,
ALARM_STATE.INACTIVE
},
reactor_ps = psil.create(), reactor_ps = psil.create(),
reactor_data = {}, ---@type reactor_db reactor_data = {}, ---@type reactor_db
@ -203,10 +221,10 @@ end
---@return boolean valid ---@return boolean valid
function iocontrol.update_statuses(statuses) function iocontrol.update_statuses(statuses)
if type(statuses) ~= "table" then if type(statuses) ~= "table" then
log.error("unit statuses not a table") log.debug("unit statuses not a table")
return false return false
elseif #statuses ~= #io.units then elseif #statuses ~= #io.units then
log.error("number of provided unit statuses does not match expected number of units") log.debug("number of provided unit statuses does not match expected number of units")
return false return false
else else
for i = 1, #statuses do for i = 1, #statuses do
@ -214,7 +232,7 @@ function iocontrol.update_statuses(statuses)
local status = statuses[i] local status = statuses[i]
if type(status) ~= "table" or #status ~= 4 then if type(status) ~= "table" or #status ~= 4 then
log.error("invalid status entry in unit statuses (not a table or invalid length)") log.debug("invalid status entry in unit statuses (not a table or invalid length)")
return false return false
end end
@ -319,16 +337,23 @@ function iocontrol.update_statuses(statuses)
local alarm_states = status[3] local alarm_states = status[3]
for id = 1, #alarm_states do if type(alarm_states) == "table" then
local state = alarm_states[id] for id = 1, #alarm_states do
local state = alarm_states[id]
if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then unit.alarms[id] = state
unit.reactor_ps.publish("ALM" .. id, 2)
elseif state == types.ALARM_STATE.RING_BACK then if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then
unit.reactor_ps.publish("ALM" .. id, 3) unit.reactor_ps.publish("ALM" .. id, 2)
else elseif state == types.ALARM_STATE.RING_BACK then
unit.reactor_ps.publish("ALM" .. id, 1) unit.reactor_ps.publish("ALM" .. id, 3)
else
unit.reactor_ps.publish("ALM" .. id, 1)
end
end end
else
log.debug("alarm states not a table")
return false
end end
-- RTU statuses -- RTU statuses
@ -377,6 +402,8 @@ function iocontrol.update_statuses(statuses)
unit.boiler_ps_tbl[id].publish(key, val) unit.boiler_ps_tbl[id].publish(key, val)
end end
end end
else
log.debug("boiler list not a table")
end end
if type(rtu_statuses.turbines) == "table" then if type(rtu_statuses.turbines) == "table" then
@ -422,9 +449,17 @@ function iocontrol.update_statuses(statuses)
unit.turbine_ps_tbl[id].publish(key, val) unit.turbine_ps_tbl[id].publish(key, val)
end end
end end
else
log.debug("turbine list not a table")
return false
end end
else
log.debug("rtu list not a table")
end end
end end
-- update alarm sounder
sounder.eval(io.units)
end end
return true return true

453
coordinator/sounder.lua Normal file
View File

@ -0,0 +1,453 @@
--
-- Alarm Sounder
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local log = require("scada-common.log")
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
---@class sounder
local sounder = {}
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 _MAX_SAMPLES = 0x20000 -- 128 * 1024 samples
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 = {
speaker = nil,
volume = 0.5,
playing = false,
num_active = 0,
next_block = 1,
quad_buffer = { {}, {}, {}, {} } -- split audio up into 0.5s samples so specific components can be ended quicker
}
-- 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
---@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
---@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
---@return boolean success successfully added buffer to audio output
local function play()
if not alarm_ctl.playing then
alarm_ctl.playing = true
alarm_ctl.next_block = 1
return sounder.continue()
else
return true
end
end
-- initialize the annunciator alarm system
---@param speaker table speaker peripheral
function sounder.init(speaker)
alarm_ctl.speaker = speaker
alarm_ctl.speaker.stop()
alarm_ctl.playing = false
alarm_ctl.num_active = 0
alarm_ctl.next_block = 1
zero()
-- 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
-- check alarm state to enable/disable alarms
---@param units table|nil unit list or nil to use test mode
function sounder.eval(units)
local changed = false
local any_active = false
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
-- check all alarms for all units
for i = 1, #units do
local unit = units[i] ---@type ioctl_entry
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
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
-- stop all audio and clear output buffer
function sounder.stop()
alarm_ctl.playing = false
alarm_ctl.speaker.stop()
alarm_ctl.next_block = 1
alarm_ctl.num_active = 0
for id = 1, #TONES do TONES[id].active = false end
zero()
end
-- continue audio on buffer empty
---@return boolean success successfully added buffer to audio output
function sounder.continue()
if alarm_ctl.playing then
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
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
else
return false
end
end
--#region Test Functions
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_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_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_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_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_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_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_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_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_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
-- 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("power rescale test took " .. (util.time_ms() - start) .. "ms")
end
--#endregion
return sounder

View File

@ -16,8 +16,9 @@ local apisessions = require("coordinator.apisessions")
local config = require("coordinator.config") local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local COORDINATOR_VERSION = "alpha-v0.7.1" local COORDINATOR_VERSION = "beta-v0.7.2"
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -91,6 +92,24 @@ local function main()
log_sys("system start on " .. os.date("%c")) log_sys("system start on " .. os.date("%c"))
log_boot("starting " .. COORDINATOR_VERSION) log_boot("starting " .. COORDINATOR_VERSION)
----------------------------------------
-- setup alarm sounder subsystem
----------------------------------------
local speaker = ppm.get_device("speaker")
if speaker == nil then
log_boot("annunciator alarm speaker not found")
println("boot> speaker not found")
log.fatal("no annunciator alarm speaker found")
return
else
local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected")
sounder.init(speaker)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured")
end
---------------------------------------- ----------------------------------------
-- setup communications -- setup communications
---------------------------------------- ----------------------------------------
@ -304,6 +323,9 @@ local function main()
elseif event == "monitor_touch" then elseif event == "monitor_touch" then
-- handle a monitor touch event -- handle a monitor touch event
renderer.handle_touch(core.events.touch(param1, param2, param3)) renderer.handle_touch(core.events.touch(param1, param2, param3))
elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied
sounder.continue()
end end
-- check for termination request -- check for termination request
@ -320,6 +342,7 @@ local function main()
end end
renderer.close_ui() renderer.close_ui()
sounder.stop()
log_sys("system shutdown") log_sys("system shutdown")
println_ts("exited") println_ts("exited")

View File

@ -250,7 +250,7 @@ local function init(parent, id)
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
end end
local t1_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t1_sdo = TriIndicatorLight{parent=annunciator,label="Steam Relief Valve",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[1].subscribe("SteamDumpOpen", function (val) t1_sdo.update(val + 1) end) t_ps[1].subscribe("SteamDumpOpen", function (val) t1_sdo.update(val + 1) end)
TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
@ -264,7 +264,7 @@ local function init(parent, id)
if unit.num_turbines > 1 then if unit.num_turbines > 1 then
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t2_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t2_sdo = TriIndicatorLight{parent=annunciator,label="Steam Relief Valve",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[2].subscribe("SteamDumpOpen", function (val) t2_sdo.update(val + 1) end) t_ps[2].subscribe("SteamDumpOpen", function (val) t2_sdo.update(val + 1) end)
TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
@ -279,7 +279,7 @@ local function init(parent, id)
if unit.num_turbines > 2 then if unit.num_turbines > 2 then
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}
local t3_sdo = TriIndicatorLight{parent=annunciator,label="Steam Dump Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t3_sdo = TriIndicatorLight{parent=annunciator,label="Steam Relief Valve",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[3].subscribe("SteamDumpOpen", function (val) t3_sdo.update(val + 1) end) t_ps[3].subscribe("SteamDumpOpen", function (val) t3_sdo.update(val + 1) end)
TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}

View File

@ -3,6 +3,7 @@
-- --
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local sounder = require("coordinator.sounder")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
@ -10,11 +11,17 @@ local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core") local core = require("graphics.core")
local ColorMap = require("graphics.elements.colormap")
local DisplayBox = require("graphics.elements.displaybox") local DisplayBox = require("graphics.elements.displaybox")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create new main view -- create new main view
---@param monitor table main viewscreen ---@param monitor table main viewscreen
local function init(monitor) local function init(monitor)
@ -41,6 +48,35 @@ local function init(monitor)
-- command & control -- command & control
-- testing
---@fixme remove test code
ColorMap{parent=main,x=2,y=(main.height()-1)}
PushButton{parent=main,x=2,y=(main.height()-20),text="TEST 1",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_1}
PushButton{parent=main,x=2,text="TEST 2",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_2}
PushButton{parent=main,x=2,text="TEST 3",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_3}
PushButton{parent=main,x=2,text="TEST 4",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_4}
PushButton{parent=main,x=2,text="TEST 5",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_5}
PushButton{parent=main,x=2,text="TEST 6",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_6}
PushButton{parent=main,x=2,text="TEST 7",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_7}
PushButton{parent=main,x=2,text="TEST 8",min_width=8,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_8}
PushButton{parent=main,x=2,text="STOP",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.stop}
PushButton{parent=main,x=2,text="PSCALE",min_width=8,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_power_scale}
SwitchButton{parent=main,x=12,y=(main.height()-20),text="CONTAINMENT BREACH",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_breach}
SwitchButton{parent=main,x=12,text="CONTAINMENT RADIATION",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rad}
SwitchButton{parent=main,x=12,text="REACTOR LOST",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_lost}
SwitchButton{parent=main,x=12,text="CRITICAL DAMAGE",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_crit}
SwitchButton{parent=main,x=12,text="REACTOR DAMAGE",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_dmg}
SwitchButton{parent=main,x=12,text="REACTOR OVER TEMP",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_overtemp}
SwitchButton{parent=main,x=12,text="REACTOR HIGH TEMP",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_hightemp}
SwitchButton{parent=main,x=12,text="REACTOR WASTE LEAK",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_wasteleak}
SwitchButton{parent=main,x=12,text="REACTOR WASTE HIGH",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_highwaste}
SwitchButton{parent=main,x=12,text="RPS TRANSIENT",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rps}
SwitchButton{parent=main,x=12,text="RCS TRANSIENT",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rcs}
SwitchButton{parent=main,x=12,text="TURBINE TRIP",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_turbinet}
return main return main
end end

View File

@ -36,7 +36,7 @@ local function switch_button(args)
-- button state (convert nil to false if missing) -- button state (convert nil to false if missing)
e.value = args.default or false e.value = args.default or false
local h_pad = math.floor((e.frame.w - text_width) / 2) local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
local v_pad = math.floor(e.frame.h / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1
-- show the button state -- show the button state
@ -51,6 +51,9 @@ local function switch_button(args)
e.window.setBackgroundColor(e.fg_bg.bkg) e.window.setBackgroundColor(e.fg_bg.bkg)
end end
-- clear to redraw background
e.window.clear()
-- write the button text -- write the button text
e.window.setCursorPos(h_pad, v_pad) e.window.setCursorPos(h_pad, v_pad)
e.window.write(args.text) e.window.write(args.text)

View File

@ -96,7 +96,7 @@ types.ALARM_PRIO_MAP = {
types.ALARM_PRIORITY.URGENT, types.ALARM_PRIORITY.URGENT,
types.ALARM_PRIORITY.CRITICAL, types.ALARM_PRIORITY.CRITICAL,
types.ALARM_PRIORITY.EMERGENCY, types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.URGENT, types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.TIMELY, types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.EMERGENCY, types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.TIMELY, types.ALARM_PRIORITY.TIMELY,

View File

@ -57,7 +57,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units)
seq_num = 0, seq_num = 0,
r_seq_num = nil, r_seq_num = nil,
connected = true, connected = true,
conn_watchdog = util.new_watchdog(3), conn_watchdog = util.new_watchdog(5),
last_rtt = 0, last_rtt = 0,
-- periodic messages -- periodic messages
periodics = { periodics = {

View File

@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions")
local config = require("supervisor.config") local config = require("supervisor.config")
local supervisor = require("supervisor.supervisor") local supervisor = require("supervisor.supervisor")
local SUPERVISOR_VERSION = "beta-v0.8.1" local SUPERVISOR_VERSION = "beta-v0.8.2"
local print = util.print local print = util.print
local println = util.println local println = util.println