From 4030fdc5c9b2f686e0f10065a7b12e8ab440368f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 4 Dec 2022 13:59:10 -0500 Subject: [PATCH] #77 alarm sounder --- coordinator/iocontrol.lua | 69 ++- coordinator/sounder.lua | 453 +++++++++++++++++++ coordinator/startup.lua | 25 +- coordinator/ui/components/unit_detail.lua | 6 +- coordinator/ui/layout/main_view.lua | 36 ++ graphics/elements/controls/switch_button.lua | 5 +- scada-common/types.lua | 2 +- supervisor/session/coordinator.lua | 2 +- supervisor/startup.lua | 2 +- 9 files changed, 575 insertions(+), 25 deletions(-) create mode 100644 coordinator/sounder.lua diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 2926289..6aa2caa 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -1,11 +1,15 @@ -local comms = require("scada-common.comms") -local log = require("scada-common.log") -local psil = require("scada-common.psil") -local types = require("scada-common.types") -local util = require("scada-common.util") +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local psil = require("scada-common.psil") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local sounder = require("coordinator.sounder") local CRDN_COMMANDS = comms.CRDN_COMMANDS +local ALARM_STATE = types.ALARM_STATE + local iocontrol = {} ---@class ioctl @@ -74,7 +78,21 @@ function iocontrol.init(conf, comms) 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_data = {}, ---@type reactor_db @@ -203,10 +221,10 @@ end ---@return boolean valid function iocontrol.update_statuses(statuses) if type(statuses) ~= "table" then - log.error("unit statuses not a table") + log.debug("unit statuses not a table") return false 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 else for i = 1, #statuses do @@ -214,7 +232,7 @@ function iocontrol.update_statuses(statuses) local status = statuses[i] 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 end @@ -319,16 +337,23 @@ function iocontrol.update_statuses(statuses) local alarm_states = status[3] - for id = 1, #alarm_states do - local state = alarm_states[id] + if type(alarm_states) == "table" then + 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.reactor_ps.publish("ALM" .. id, 2) - elseif state == types.ALARM_STATE.RING_BACK then - unit.reactor_ps.publish("ALM" .. id, 3) - else - unit.reactor_ps.publish("ALM" .. id, 1) + unit.alarms[id] = state + + if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then + unit.reactor_ps.publish("ALM" .. id, 2) + elseif state == types.ALARM_STATE.RING_BACK then + unit.reactor_ps.publish("ALM" .. id, 3) + else + unit.reactor_ps.publish("ALM" .. id, 1) + end end + else + log.debug("alarm states not a table") + return false end -- RTU statuses @@ -377,6 +402,8 @@ function iocontrol.update_statuses(statuses) unit.boiler_ps_tbl[id].publish(key, val) end end + else + log.debug("boiler list not a table") end if type(rtu_statuses.turbines) == "table" then @@ -422,9 +449,17 @@ function iocontrol.update_statuses(statuses) unit.turbine_ps_tbl[id].publish(key, val) end end + else + log.debug("turbine list not a table") + return false end + else + log.debug("rtu list not a table") end end + + -- update alarm sounder + sounder.eval(io.units) end return true diff --git a/coordinator/sounder.lua b/coordinator/sounder.lua new file mode 100644 index 0000000..7e7b779 --- /dev/null +++ b/coordinator/sounder.lua @@ -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 diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 225c681..1be9aa6 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -16,8 +16,9 @@ local apisessions = require("coordinator.apisessions") local config = require("coordinator.config") local coordinator = require("coordinator.coordinator") 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 println = util.println @@ -91,6 +92,24 @@ local function main() log_sys("system start on " .. os.date("%c")) 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 ---------------------------------------- @@ -304,6 +323,9 @@ local function main() elseif event == "monitor_touch" then -- handle a monitor touch event renderer.handle_touch(core.events.touch(param1, param2, param3)) + elseif event == "speaker_audio_empty" then + -- handle speaker buffer emptied + sounder.continue() end -- check for termination request @@ -320,6 +342,7 @@ local function main() end renderer.close_ui() + sounder.stop() log_sys("system shutdown") println_ts("exited") diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index d72b9fc..c2ebf16 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -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)} 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) 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 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) 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 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) TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index b9145a3..0a70971 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -3,6 +3,7 @@ -- local iocontrol = require("coordinator.iocontrol") +local sounder = require("coordinator.sounder") 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 ColorMap = require("graphics.elements.colormap") local DisplayBox = require("graphics.elements.displaybox") 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 cpair = core.graphics.cpair + -- create new main view ---@param monitor table main viewscreen local function init(monitor) @@ -41,6 +48,35 @@ local function init(monitor) -- 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 end diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua index cb003cb..46364b5 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -36,7 +36,7 @@ local function switch_button(args) -- button state (convert nil to false if missing) 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 -- show the button state @@ -51,6 +51,9 @@ local function switch_button(args) e.window.setBackgroundColor(e.fg_bg.bkg) end + -- clear to redraw background + e.window.clear() + -- write the button text e.window.setCursorPos(h_pad, v_pad) e.window.write(args.text) diff --git a/scada-common/types.lua b/scada-common/types.lua index 2ef80b3..abc7561 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -96,7 +96,7 @@ types.ALARM_PRIO_MAP = { types.ALARM_PRIORITY.URGENT, types.ALARM_PRIORITY.CRITICAL, types.ALARM_PRIORITY.EMERGENCY, - types.ALARM_PRIORITY.URGENT, + types.ALARM_PRIORITY.EMERGENCY, types.ALARM_PRIORITY.TIMELY, types.ALARM_PRIORITY.EMERGENCY, types.ALARM_PRIORITY.TIMELY, diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 4dac34a..1aaf384 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -57,7 +57,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) seq_num = 0, r_seq_num = nil, connected = true, - conn_watchdog = util.new_watchdog(3), + conn_watchdog = util.new_watchdog(5), last_rtt = 0, -- periodic messages periodics = { diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 3d27727..6257679 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.8.1" +local SUPERVISOR_VERSION = "beta-v0.8.2" local print = util.print local println = util.println