From d4ae18eee7bb2dce8b807f029ce620ec74bd0c72 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 26 Nov 2022 16:18:31 -0500 Subject: [PATCH] #10 #133 alarm system logic and display, change to comms to support alarm actions, get_x get_y to graphics elements, bugfixes to coord establish and rtu establish, flashing trilight and alarm light indicators --- coordinator/coordinator.lua | 6 + coordinator/iocontrol.lua | 83 +++++- coordinator/startup.lua | 2 +- coordinator/ui/components/unit_detail.lua | 234 +++++++++++++---- graphics/element.lua | 13 + graphics/elements/indicators/alight.lua | 113 ++++++++ graphics/elements/indicators/trilight.lua | 51 +++- reactor-plc/startup.lua | 2 +- rtu/rtu.lua | 2 +- rtu/startup.lua | 2 +- scada-common/alarm.lua | 73 ------ scada-common/comms.lua | 7 +- scada-common/types.lua | 70 +++++ supervisor/session/coordinator.lua | 19 +- supervisor/session/unit.lua | 305 +++++++++++++++++++++- supervisor/startup.lua | 2 +- 16 files changed, 824 insertions(+), 160 deletions(-) create mode 100644 graphics/elements/indicators/alight.lua delete mode 100644 scada-common/alarm.lua diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 2a211cb..5662cd6 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -421,6 +421,8 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa unit.set_burn_ack(ack) elseif cmd == CRDN_COMMANDS.SET_WASTE then unit.set_waste_ack(ack) + elseif cmd == CRDN_COMMANDS.ACK_ALL_ALARMS then + unit.ack_alarms_ack(ack) else log.debug(util.c("received command ack with unknown command ", cmd)) end @@ -474,6 +476,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa else log.debug("supervisor connection denied") end + elseif packet.length == 1 and packet.data[1] == ESTABLISH_ACK.DENY then + log.debug("supervisor connection denied") + elseif packet.length == 1 and packet.data[1] == ESTABLISH_ACK.COLLISION then + log.debug("supervisor connection denied due to collision") else log.debug("SCADA_MGMT establish packet length mismatch") end diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 8f461cc..67727cb 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -1,6 +1,7 @@ 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 CRDN_COMMANDS = comms.CRDN_COMMANDS @@ -22,9 +23,19 @@ function iocontrol.init(conf, comms) io.units = {} for i = 1, conf.num_units do + local function ack(alarm) + comms.send_command(CRDN_COMMANDS.ACK_ALARM, i, alarm) + log.debug(util.c("UNIT[", i, "]: ACK ALARM ", alarm)) + end + + local function reset(alarm) + comms.send_command(CRDN_COMMANDS.RESET_ALARM, i, alarm) + log.debug(util.c("UNIT[", i, "]: RESET ALARM ", alarm)) + end + ---@class ioctl_entry local entry = { - unit_id = i, ---@type integer + unit_id = i, ---@type integer initialized = false, num_boilers = 0, @@ -37,17 +48,36 @@ function iocontrol.init(conf, comms) start = function () end, scram = function () end, reset_rps = function () end, - set_burn = function (rate) end, - set_waste = function (mode) end, + ack_alarms = function () end, + set_burn = function (rate) end, ---@param rate number + set_waste = function (mode) end, ---@param mode integer - start_ack = function (success) end, - scram_ack = function (success) end, - reset_rps_ack = function (success) end, - set_burn_ack = function (success) end, - set_waste_ack = function (success) end, + start_ack = function (success) end, ---@param success boolean + scram_ack = function (success) end, ---@param success boolean + reset_rps_ack = function (success) end, ---@param success boolean + ack_alarms_ack = function (success) end,---@param success boolean + set_burn_ack = function (success) end, ---@param success boolean + set_waste_ack = function (success) end, ---@param success boolean + + alarm_callbacks = { + c_breach = { ack = function () ack(1) end, reset = function () reset(1) end }, + radiation = { ack = function () ack(2) end, reset = function () reset(2) end }, + dmg_crit = { ack = function () ack(3) end, reset = function () reset(3) end }, + r_lost = { ack = function () ack(4) end, reset = function () reset(4) end }, + damage = { ack = function () ack(5) end, reset = function () reset(5) end }, + over_temp = { ack = function () ack(6) end, reset = function () reset(6) end }, + high_temp = { ack = function () ack(7) end, reset = function () reset(7) end }, + waste_leak = { ack = function () ack(8) end, reset = function () reset(8) end }, + waste_high = { ack = function () ack(9) end, reset = function () reset(9) end }, + rps_trans = { ack = function () ack(10) end, reset = function () reset(10) end }, + rcs_trans = { ack = function () ack(11) end, reset = function () reset(11) end }, + t_trip = { ack = function () ack(12) end, reset = function () reset(12) end } + }, + + alarms = {}, ---@type alarms reactor_ps = psil.create(), - reactor_data = {}, ---@type reactor_db + reactor_data = {}, ---@type reactor_db boiler_ps_tbl = {}, boiler_data_tbl = {}, @@ -73,6 +103,11 @@ function iocontrol.init(conf, comms) log.debug(util.c("UNIT[", i, "]: RESET_RPS")) end + function entry.ack_alarms() + comms.send_command(CRDN_COMMANDS.ACK_ALL_ALARMS, i) + log.debug(util.c("UNIT[", i, "]: ACK_ALL_ALARMS")) + end + function entry.set_burn(rate) comms.send_command(CRDN_COMMANDS.SET_BURN, i, rate) log.debug(util.c("UNIT[", i, "]: SET_BURN = ", rate)) @@ -167,7 +202,10 @@ end ---@param statuses table ---@return boolean valid function iocontrol.update_statuses(statuses) - if #statuses ~= #io.units then + if type(statuses) ~= "table" then + log.error("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") return false else @@ -175,6 +213,11 @@ function iocontrol.update_statuses(statuses) local unit = io.units[i] ---@type ioctl_entry 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)") + return false + end + -- reactor PLC status local reactor_status = status[1] @@ -253,7 +296,7 @@ function iocontrol.update_statuses(statuses) end unit.reactor_ps.publish("TurbineTrip", any) - elseif key == "BoilerOnline" or key == "HeatingRateLow" then + elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then -- split up array for all boilers for id = 1, #val do unit.boiler_ps_tbl[id].publish(key, val[id]) @@ -272,9 +315,25 @@ function iocontrol.update_statuses(statuses) end end + -- alarms + + local alarm_states = status[3] + + 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) + end + end + -- RTU statuses - local rtu_statuses = status[3] + local rtu_statuses = status[4] if type(rtu_statuses) == "table" then if type(rtu_statuses.boilers) == "table" then diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 6cc3481..b82fa4f 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -17,7 +17,7 @@ local config = require("coordinator.config") local coordinator = require("coordinator.coordinator") local renderer = require("coordinator.renderer") -local COORDINATOR_VERSION = "alpha-v0.6.17" +local COORDINATOR_VERSION = "alpha-v0.7.0" local print = util.print local println = util.println diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index c036827..d72b9fc 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -2,8 +2,6 @@ -- Reactor Unit SCADA Coordinator GUI -- -local tcallbackdsp = require("scada-common.tcallbackdsp") - local iocontrol = require("coordinator.iocontrol") local style = require("coordinator.ui.style") @@ -12,8 +10,8 @@ local core = require("graphics.core") local Div = require("graphics.elements.div") local TextBox = require("graphics.elements.textbox") -local ColorMap = require("graphics.elements.colormap") +local AlarmLight = require("graphics.elements.indicators.alight") local CoreMap = require("graphics.elements.indicators.coremap") local DataIndicator = require("graphics.elements.indicators.data") local IndicatorLight = require("graphics.elements.indicators.light") @@ -30,6 +28,29 @@ local cpair = core.graphics.cpair local period = core.flasher.PERIOD +local waste_opts = { + { + text = "Auto", + fg_bg = cpair(colors.black, colors.lightGray), + active_fg_bg = cpair(colors.white, colors.gray) + }, + { + text = "Pu", + fg_bg = cpair(colors.black, colors.lightGray), + active_fg_bg = cpair(colors.black, colors.green) + }, + { + text = "Po", + fg_bg = cpair(colors.black, colors.lightGray), + active_fg_bg = cpair(colors.black, colors.cyan) + }, + { + text = "AM", + fg_bg = cpair(colors.black, colors.lightGray), + active_fg_bg = cpair(colors.black, colors.purple) + } +} + -- create a unit view ---@param parent graphics_element parent ---@param id integer @@ -43,10 +64,12 @@ local function init(parent, id) TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} - local scram_fg_bg = cpair(colors.white, colors.gray) - local lu_cpair = cpair(colors.gray, colors.gray) + local hzd_fg_bg = cpair(colors.white, colors.gray) + local lu_cpair = cpair(colors.gray, colors.gray) + ----------------------------- -- main stats and core map -- + ----------------------------- local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18} r_ps.subscribe("temp", core_map.update) @@ -84,18 +107,20 @@ local function init(parent, id) DataIndicator{parent=main,x=21,label="",format="%6.2f",value=0,unit="mSv/h",lu_colors=lu_cpair,width=12,fg_bg=stat_fg_bg} main.line_break() + ----------------- -- annunciator -- + ----------------- - local annunciator = Div{parent=main,x=34,y=3} + -- annunciator colors (generally) per IAEA-TECDOC-812 recommendations - -- annunciator colors per IAEA-TECDOC-812 recommendations + local annunciator = Div{parent=main,x=35,y=3} -- connectivity/basic state local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)} local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)} local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)} ---@todo auto control as info sent here - local r_auto = IndicatorLight{parent=annunciator,label="Auto Control",colors=cpair(colors.blue,colors.gray)} + local r_auto = IndicatorLight{parent=annunciator,label="Auto. Control",colors=cpair(colors.blue,colors.gray)} r_ps.subscribe("PLCOnline", plc_online.update) r_ps.subscribe("PLCHeartbeat", plc_hbeat.update) @@ -103,23 +128,25 @@ local function init(parent, id) annunciator.line_break() - -- annunciator fields + -- non-RPS reactor annunciator panel local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)} local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)} local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=cpair(colors.red,colors.gray)} local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)} local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)} + local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=cpair(colors.yellow,colors.gray)} local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)} local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)} local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)} local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)} - local r_hsrt = IndicatorLight{parent=annunciator,label="High Startup Rate",colors=cpair(colors.yellow,colors.gray)} + local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)} r_ps.subscribe("ReactorSCRAM", r_scram.update) r_ps.subscribe("ManualReactorSCRAM", r_mscrm.update) r_ps.subscribe("AutoReactorSCRAM", r_ascrm.update) r_ps.subscribe("RCPTrip", r_rtrip.update) r_ps.subscribe("RCSFlowLow", r_cflow.update) + r_ps.subscribe("CoolantLevelLow", r_clow.update) r_ps.subscribe("ReactorTempHigh", r_temp.update) r_ps.subscribe("ReactorHighDeltaT", r_rhdt.update) r_ps.subscribe("FuelInputRateLow", r_firl.update) @@ -128,14 +155,24 @@ local function init(parent, id) annunciator.line_break() - -- RPS + -- RPS annunciator panel + TextBox{parent=main,x=34,y=20,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.brown)} local rps_trp = IndicatorLight{parent=annunciator,label="RPS Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_dmg = IndicatorLight{parent=annunciator,label="Damage Critical",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_exh = IndicatorLight{parent=annunciator,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)} local rps_exw = IndicatorLight{parent=annunciator,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)} - local rps_tmp = IndicatorLight{parent=annunciator,label="High Core Temp",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} + local rps_tmp = IndicatorLight{parent=annunciator,label="Core Temp. High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local rps_nof = IndicatorLight{parent=annunciator,label="No Fuel",colors=cpair(colors.yellow,colors.gray)} - local rps_noc = IndicatorLight{parent=annunciator,label="No Coolant",colors=cpair(colors.yellow,colors.gray)} + local rps_noc = IndicatorLight{parent=annunciator,label="Coolant Level Low Low",colors=cpair(colors.yellow,colors.gray)} local rps_flt = IndicatorLight{parent=annunciator,label="PPM Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS} local rps_tmo = IndicatorLight{parent=annunciator,label="Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS} local rps_sfl = IndicatorLight{parent=annunciator,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS} @@ -153,7 +190,12 @@ local function init(parent, id) annunciator.line_break() - -- cooling + -- cooling annunciator panel + TextBox{parent=main,x=34,y=31,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} + TextBox{parent=main,x=34,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)} local c_cfm = IndicatorLight{parent=annunciator,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_brm = IndicatorLight{parent=annunciator,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_sfm = IndicatorLight{parent=annunciator,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} @@ -168,16 +210,31 @@ local function init(parent, id) annunciator.line_break() - -- machine-specific indicators + -- boiler annunciator panel(s) + + local tag_y = 1 + if unit.num_boilers > 0 then - TextBox{parent=main,x=32,y=36,text="B1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + tag_y = TextBox{parent=main,x=32,y=37,text="B1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() + local b1_wll = IndicatorLight{parent=annunciator,label="Water Level Low",colors=cpair(colors.red,colors.gray)} + b_ps[1].subscribe("WasterLevelLow", b1_wll.update) + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} + + tag_y = TextBox{parent=main,x=32,text="B1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() local b1_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)} b_ps[1].subscribe("HeatingRateLow", b1_hr.update) + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} end if unit.num_boilers > 1 then - TextBox{parent=main,x=32,text="B2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + tag_y = TextBox{parent=main,x=32,text="B2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() + local b2_wll = IndicatorLight{parent=annunciator,label="Water Level Low",colors=cpair(colors.red,colors.gray)} + b_ps[2].subscribe("WasterLevelLow", b2_wll.update) + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} + + tag_y = TextBox{parent=main,x=32,text="B2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() local b2_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)} b_ps[2].subscribe("HeatingRateLow", b2_hr.update) + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.blue)} end if unit.num_boilers > 0 then @@ -185,7 +242,14 @@ local function init(parent, id) annunciator.line_break() end - TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + -- turbine annunciator panels + + if unit.num_boilers == 0 then + TextBox{parent=main,x=32,y=37,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + else + 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} t_ps[1].subscribe("SteamDumpOpen", function (val) t1_sdo.update(val + 1) end) @@ -193,12 +257,10 @@ local function init(parent, id) local t1_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update) - TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + tag_y = TextBox{parent=main,x=32,text="T1",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() local t1_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} t_ps[1].subscribe("TurbineTrip", t1_trp.update) - - main.line_break() - annunciator.line_break() + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)} if unit.num_turbines > 1 then TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} @@ -209,12 +271,10 @@ local function init(parent, id) local t2_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update) - TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + tag_y = TextBox{parent=main,x=32,text="T2",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() local t2_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} t_ps[2].subscribe("TurbineTrip", t2_trp.update) - - main.line_break() - annunciator.line_break() + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)} end if unit.num_turbines > 2 then @@ -226,18 +286,20 @@ local function init(parent, id) local t3_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update) - TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)} + tag_y = TextBox{parent=main,x=32,text="T3",width=2,height=1,fg_bg=cpair(colors.black, colors.white)}.get_y() local t3_trp = IndicatorLight{parent=annunciator,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} t_ps[3].subscribe("TurbineTrip", t3_trp.update) - - annunciator.line_break() + TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)} end + annunciator.line_break() + ---@todo radiation monitor IndicatorLight{parent=annunciator,label="Radiation Monitor",colors=cpair(colors.green,colors.gray)} - IndicatorLight{parent=annunciator,label="Radiation Alarm",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} + ---------------------- -- reactor controls -- + ---------------------- local burn_control = Div{parent=main,x=2,y=22,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)} local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=cpair(colors.black,colors.white)} @@ -251,13 +313,15 @@ local function init(parent, id) local dis_colors = cpair(colors.white, colors.lightGray) - local start = HazardButton{parent=main,x=2,y=26,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=scram_fg_bg} - local scram = HazardButton{parent=main,x=12,y=26,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=scram_fg_bg} - local reset = HazardButton{parent=main,x=22,y=26,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=scram_fg_bg} + local start = HazardButton{parent=main,x=22,y=22,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg} + local ack_a = HazardButton{parent=main,x=12,y=26,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg} + local scram = HazardButton{parent=main,x=2,y=26,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg} + local reset = HazardButton{parent=main,x=22,y=26,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg} unit.start_ack = start.on_response unit.scram_ack = scram.on_response unit.reset_rps_ack = reset.on_response + unit.ack_alarms_ack = ack_a.on_response local function start_button_en_check() if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then @@ -270,35 +334,89 @@ local function init(parent, id) r_ps.subscribe("rps_tripped", start_button_en_check) r_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) - local opts = { - { - text = "Auto", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.white, colors.gray) - }, - { - text = "Pu", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.black, colors.lime) - }, - { - text = "Po", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.black, colors.cyan) - }, - { - text = "AM", - fg_bg = cpair(colors.black, colors.lightGray), - active_fg_bg = cpair(colors.black, colors.purple) - } - } + TextBox{parent=main,x=2,y=30,text="Idle",width=29,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)} - ---@todo waste selection - local waste_sel = Div{parent=main,x=2,y=48,width=29,height=2,fg_bg=cpair(colors.black, colors.white)} + local waste_sel = Div{parent=main,x=2,y=50,width=29,height=2,fg_bg=cpair(colors.black, colors.white)} - MultiButton{parent=waste_sel,x=1,y=1,options=opts,callback=unit.set_waste,min_width=6,fg_bg=cpair(colors.black, colors.white)} + MultiButton{parent=waste_sel,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6,fg_bg=cpair(colors.black, colors.white)} TextBox{parent=waste_sel,text="Waste Processing",alignment=TEXT_ALIGN.CENTER,x=1,y=1,height=1} + ---------------------- + -- alarm management -- + ---------------------- + + local alarm_panel = Div{parent=main,x=2,y=32,width=29,height=16,fg_bg=cpair(colors.black,colors.white)} + + local a_brc = AlarmLight{parent=alarm_panel,x=6,y=2,label="Containment Breach",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + local a_rad = AlarmLight{parent=alarm_panel,x=6,label="Containment Radiation",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + local a_dmg = AlarmLight{parent=alarm_panel,x=6,label="Critical Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + alarm_panel.line_break() + local a_rcl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Lost",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + local a_rcd = AlarmLight{parent=alarm_panel,x=6,label="Reactor Damage",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + local a_rot = AlarmLight{parent=alarm_panel,x=6,label="Reactor Over Temp",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + local a_rht = AlarmLight{parent=alarm_panel,x=6,label="Reactor High Temp",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS} + local a_rwl = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste Leak",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + local a_rwh = AlarmLight{parent=alarm_panel,x=6,label="Reactor Waste High",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS} + alarm_panel.line_break() + local a_rps = AlarmLight{parent=alarm_panel,x=6,label="RPS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS} + local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS} + local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS} + + r_ps.subscribe("ALM1", a_brc.update) + r_ps.subscribe("ALM2", a_rad.update) + r_ps.subscribe("ALM4", a_dmg.update) + + r_ps.subscribe("ALM3", a_rcl.update) + r_ps.subscribe("ALM5", a_rcd.update) + r_ps.subscribe("ALM6", a_rot.update) + r_ps.subscribe("ALM7", a_rht.update) + r_ps.subscribe("ALM8", a_rwl.update) + r_ps.subscribe("ALM9", a_rwh.update) + + r_ps.subscribe("ALM10", a_rps.update) + r_ps.subscribe("ALM11", a_clt.update) + r_ps.subscribe("ALM12", a_tbt.update) + + -- ack's and resets + + local c = unit.alarm_callbacks + local ack_fg_bg = cpair(colors.black, colors.orange) + local rst_fg_bg = cpair(colors.black, colors.lime) + local active_fg_bg = cpair(colors.white, colors.gray) + + PushButton{parent=alarm_panel,x=2,y=2,text="\x13",min_width=1,callback=c.c_breach.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=2,text="R",min_width=1,callback=c.c_breach.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=3,text="\x13",min_width=1,callback=c.radiation.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=3,text="R",min_width=1,callback=c.radiation.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=4,text="\x13",min_width=1,callback=c.dmg_crit.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=4,text="R",min_width=1,callback=c.dmg_crit.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + + PushButton{parent=alarm_panel,x=2,y=6,text="\x13",min_width=1,callback=c.r_lost.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=6,text="R",min_width=1,callback=c.r_lost.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=7,text="\x13",min_width=1,callback=c.damage.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=7,text="R",min_width=1,callback=c.damage.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=8,text="\x13",min_width=1,callback=c.over_temp.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=8,text="R",min_width=1,callback=c.over_temp.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=9,text="\x13",min_width=1,callback=c.high_temp.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=9,text="R",min_width=1,callback=c.high_temp.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=10,text="\x13",min_width=1,callback=c.waste_leak.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=10,text="R",min_width=1,callback=c.waste_leak.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=11,text="\x13",min_width=1,callback=c.waste_high.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=11,text="R",min_width=1,callback=c.waste_high.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + + PushButton{parent=alarm_panel,x=2,y=13,text="\x13",min_width=1,callback=c.rps_trans.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=13,text="R",min_width=1,callback=c.rps_trans.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=14,text="\x13",min_width=1,callback=c.rcs_trans.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=14,text="R",min_width=1,callback=c.rcs_trans.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=2,y=15,text="\x13",min_width=1,callback=c.t_trip.ack,fg_bg=ack_fg_bg,active_fg_bg=active_fg_bg} + PushButton{parent=alarm_panel,x=4,y=15,text="R",min_width=1,callback=c.t_trip.reset,fg_bg=rst_fg_bg,active_fg_bg=active_fg_bg} + + -- color tags + + TextBox{parent=alarm_panel,x=5,y=13,text="\x95",width=1,height=1,fg_bg=cpair(colors.white, colors.brown)} + TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white, colors.blue)} + TextBox{parent=alarm_panel,x=5,text="\x95",width=1,height=1,fg_bg=cpair(colors.white, colors.cyan)} + return main end diff --git a/graphics/element.lua b/graphics/element.lua index d987a7a..7bf8b71 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -26,6 +26,7 @@ local element = {} ---|push_button_args ---|spinbox_args ---|switch_button_args +---|alarm_indicator_light ---|core_map_args ---|data_indicator_args ---|hbar_args @@ -302,6 +303,18 @@ function element.new(args) ---@return cpair fg_bg function public.get_fg_bg() return protected.fg_bg end + -- get element x + ---@return integer x + function public.get_x() + return protected.frame.x + end + + -- get element y + ---@return integer y + function public.get_y() + return protected.frame.y + end + -- get element width ---@return integer width function public.width() diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua new file mode 100644 index 0000000..eea103a --- /dev/null +++ b/graphics/elements/indicators/alight.lua @@ -0,0 +1,113 @@ +-- Tri-State Alarm Indicator Light Graphics Element + +local util = require("scada-common.util") + +local element = require("graphics.element") +local flasher = require("graphics.flasher") + +---@class alarm_indicator_light +---@field label string indicator label +---@field c1 color color for off state +---@field c2 color color for alarm state +---@field c3 color color for ring-back state +---@field min_label_width? integer label length if omitted +---@field flash? boolean whether to flash on alarm state rather than stay on +---@field period? PERIOD flash period +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@field fg_bg? cpair foreground/background colors + +-- new alarm indicator light +---@param args alarm_indicator_light +---@return graphics_element element, element_id id +local function alarm_indicator_light(args) + assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field") + assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field") + assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field") + assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field") + + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled") + end + + -- single line + args.height = 1 + + -- determine width + args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + + -- flasher state + local flash_on = true + + -- blit translations + local c1 = colors.toBlit(args.c1) + local c2 = colors.toBlit(args.c2) + local c3 = colors.toBlit(args.c3) + + -- create new graphics element base object + local e = element.new(args) + + -- called by flasher when enabled + local function flash_callback() + e.window.setCursorPos(1, 1) + + if flash_on then + if e.value == 2 then + e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + end + else + if e.value == 3 then + e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + else + e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + end + end + + flash_on = not flash_on + end + + -- on state change + ---@param new_state integer indicator state + function e.on_update(new_state) + local was_off = e.value ~= 2 + + e.value = new_state + e.window.setCursorPos(1, 1) + + if args.flash then + if was_off and (new_state == 2) then + flash_on = true + flasher.start(flash_callback, args.period) + elseif new_state ~= 2 then + flash_on = false + flasher.stop(flash_callback) + + if new_state == 3 then + e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + else + e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + end + end + elseif new_state == 2 then + e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + elseif new_state == 3 then + e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + else + e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + end + end + + -- set indicator state + ---@param val integer indicator state + function e.set_value(val) e.on_update(val) end + + -- write label and initial indicator light + e.on_update(1) + e.window.write(args.label) + + return e.get() +end + +return alarm_indicator_light diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua index 83aef37..2c61fb7 100644 --- a/graphics/elements/indicators/trilight.lua +++ b/graphics/elements/indicators/trilight.lua @@ -1,6 +1,9 @@ -- Tri-State Indicator Light Graphics Element +local util = require("scada-common.util") + local element = require("graphics.element") +local flasher = require("graphics.flasher") ---@class tristate_indicator_light_args ---@field label string indicator label @@ -8,13 +11,15 @@ local element = require("graphics.element") ---@field c2 color color for state 2 ---@field c3 color color for state 3 ---@field min_label_width? integer label length if omitted +---@field flash? boolean whether to flash on state 2 or 3 rather than stay on +---@field period? PERIOD flash period ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted ---@field y? integer 1 if omitted ---@field fg_bg? cpair foreground/background colors --- new indicator light +-- new tri-state indicator light ---@param args tristate_indicator_light_args ---@return graphics_element element, element_id id local function tristate_indicator_light(args) @@ -23,12 +28,19 @@ local function tristate_indicator_light(args) assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field") assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field") + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled") + end + -- single line args.height = 1 -- determine width args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + -- flasher state + local flash_on = true + -- blit translations local c1 = colors.toBlit(args.c1) local c2 = colors.toBlit(args.c2) @@ -37,12 +49,45 @@ local function tristate_indicator_light(args) -- create new graphics element base object local e = element.new(args) + -- init value for initial check in on_update + e.value = 1 + + -- called by flasher when enabled + local function flash_callback() + e.window.setCursorPos(1, 1) + + if flash_on then + if e.value == 2 then + e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + elseif e.value == 3 then + e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + end + else + e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + end + + flash_on = not flash_on + end + -- on state change ---@param new_state integer indicator state function e.on_update(new_state) + local was_off = e.value <= 1 + e.value = new_state e.window.setCursorPos(1, 1) - if new_state == 2 then + + if args.flash then + if was_off and (new_state > 1) then + flash_on = true + flasher.start(flash_callback, args.period) + elseif new_state <= 1 then + flash_on = false + flasher.stop(flash_callback) + + e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + end + elseif new_state == 2 then e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) elseif new_state == 3 then e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) @@ -56,7 +101,7 @@ local function tristate_indicator_light(args) function e.set_value(val) e.on_update(val) end -- write label and initial indicator light - e.on_update(0) + e.on_update(1) e.window.write(args.label) return e.get() diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 3f6c0d5..752f795 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.9.7" +local R_PLC_VERSION = "beta-v0.9.8" local print = util.print local println = util.println diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 88516d1..2831cf4 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -412,7 +412,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog) else -- establish denied public.unlink(rtu_state) - println_ts("supervisor connection") + println_ts("supervisor connection denied") log.warning("supervisor connection denied by remote host") end else diff --git a/rtu/startup.lua b/rtu/startup.lua index 67a9867..2e2272d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.9.5" +local RTU_VERSION = "beta-v0.9.6" local rtu_t = types.rtu_t diff --git a/scada-common/alarm.lua b/scada-common/alarm.lua deleted file mode 100644 index 0a3038c..0000000 --- a/scada-common/alarm.lua +++ /dev/null @@ -1,73 +0,0 @@ -local util = require("scada-common.util") - ----@class alarm -local alarm = {} - ----@alias SEVERITY integer -SEVERITY = { - INFO = 0, -- basic info message - WARNING = 1, -- warning about some abnormal state - ALERT = 2, -- important device state changes - FACILITY = 3, -- facility-wide alert - SAFETY = 4, -- safety alerts - EMERGENCY = 5 -- critical safety alarm -} - -alarm.SEVERITY = SEVERITY - --- severity integer to string ----@param severity SEVERITY -function alarm.severity_to_string(severity) - if severity == SEVERITY.INFO then - return "INFO" - elseif severity == SEVERITY.WARNING then - return "WARNING" - elseif severity == SEVERITY.ALERT then - return "ALERT" - elseif severity == SEVERITY.FACILITY then - return "FACILITY" - elseif severity == SEVERITY.SAFETY then - return "SAFETY" - elseif severity == SEVERITY.EMERGENCY then - return "EMERGENCY" - else - return "UNKNOWN" - end -end - --- create a new scada alarm entry ----@param severity SEVERITY ----@param device string ----@param message string -function alarm.scada_alarm(severity, device, message) - local self = { - time = util.time(), - ts_string = os.date("[%H:%M:%S]"), - severity = severity, - device = device, - message = message - } - - ---@class scada_alarm - local public = {} - - -- format the alarm as a string - ---@return string message - function public.format() - return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device .. ") >> " .. self.message - end - - -- get alarm properties - function public.properties() - return { - time = self.time, - severity = self.severity, - device = self.device, - message = self.message - } - end - - return public -end - -return alarm diff --git a/scada-common/comms.lua b/scada-common/comms.lua index adf58c4..f25df22 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -12,7 +12,7 @@ local rtu_t = types.rtu_t local insert = table.insert -comms.version = "1.0.0" +comms.version = "1.0.1" ---@alias PROTOCOLS integer local PROTOCOLS = { @@ -74,7 +74,10 @@ local CRDN_COMMANDS = { START = 1, -- start the reactor RESET_RPS = 2, -- reset the RPS SET_BURN = 3, -- set the burn rate - SET_WASTE = 4 -- set the waste processing mode + SET_WASTE = 4, -- set the waste processing mode + ACK_ALL_ALARMS = 5, -- ack all active alarms + ACK_ALARM = 6, -- ack a particular alarm + RESET_ALARM = 7 -- reset a particular alarm } ---@alias CAPI_TYPES integer diff --git a/scada-common/types.lua b/scada-common/types.lua index b86aca7..eaeced6 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -35,6 +35,76 @@ types.TRI_FAIL = { FULL = 2 } +---@alias ALARM integer +types.ALARM = { + ContainmentBreach = 1, + ContainmentRadiation = 2, + ReactorLost = 3, + CriticalDamage = 4, + ReactorDamage = 5, + ReactorOverTemp = 6, + ReactorHighTemp = 7, + ReactorWasteLeak = 8, + ReactorHighWaste = 9, + RPSTransient = 10, + RCSTransient = 11, + TurbineTrip = 12 +} + +types.alarm_string = { + "ContainmentBreach", + "ContainmentRadiation", + "ReactorLost", + "CriticalDamage", + "ReactorDamage", + "ReactorOverTemp", + "ReactorHighTemp", + "ReactorWasteLeak", + "ReactorHighWaste", + "RPSTransient", + "RCSTransient", + "TurbineTrip" +} + +---@alias ALARM_PRIORITY integer +types.ALARM_PRIORITY = { + CRITICAL = 0, + EMERGENCY = 1, + URGENT = 2, + TIMELY = 3 +} + +types.alarm_prio_string = { + "CRITICAL", + "EMERGENCY", + "URGENT", + "TIMELY" +} + +-- map alarms to alarm priority +types.ALARM_PRIO_MAP = { + types.ALARM_PRIORITY.CRITICAL, + types.ALARM_PRIORITY.CRITICAL, + types.ALARM_PRIORITY.URGENT, + types.ALARM_PRIORITY.CRITICAL, + types.ALARM_PRIORITY.EMERGENCY, + types.ALARM_PRIORITY.URGENT, + types.ALARM_PRIORITY.TIMELY, + types.ALARM_PRIORITY.EMERGENCY, + types.ALARM_PRIORITY.TIMELY, + types.ALARM_PRIORITY.URGENT, + types.ALARM_PRIORITY.TIMELY, + types.ALARM_PRIORITY.URGENT +} + +---@alias ALARM_STATE integer +types.ALARM_STATE = { + INACTIVE = 0, + TRIPPED = 1, + ACKED = 2, + RING_BACK = 3 +} + -- STRING TYPES -- ---@alias os_event diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index 53ba62c..675c481 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -129,7 +129,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) for i = 1, #self.units do local unit = self.units[i] ---@type reactor_unit - status[unit.get_id()] = { unit.get_reactor_status(), unit.get_annunciator(), unit.get_rtu_statuses() } + status[unit.get_id()] = { unit.get_reactor_status(), unit.get_annunciator(), unit.get_alarms(), unit.get_rtu_statuses() } end _send(SCADA_CRDN_TYPES.UNIT_STATUSES, status) @@ -191,6 +191,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) -- continue if valid unit id if util.is_int(uid) and uid > 0 and uid <= #self.units then + local unit = self.units[uid] ---@type reactor_unit + if cmd == CRDN_COMMANDS.START then self.out_q.push_data(SV_Q_DATA.START, data) elseif cmd == CRDN_COMMANDS.SCRAM then @@ -209,6 +211,21 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units) else log.debug(log_header .. "CRDN command unit set waste missing option") end + elseif cmd == CRDN_COMMANDS.ACK_ALL_ALARMS then + unit.ack_all() + _send(SCADA_CRDN_TYPES.COMMAND_UNIT, { cmd, uid, true }) + elseif cmd == CRDN_COMMANDS.ACK_ALARM then + if pkt.length == 3 then + unit.ack_alarm(pkt.data[3]) + else + log.debug(log_header .. "CRDN command unit ack alarm missing id") + end + elseif cmd == CRDN_COMMANDS.RESET_ALARM then + if pkt.length == 3 then + unit.reset_alarm(pkt.data[3]) + else + log.debug(log_header .. "CRDN command unit reset alarm missing id") + end else log.debug(log_header .. "CRDN command unknown") end diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index 2b80cb1..da74b30 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -4,9 +4,15 @@ local log = require("scada-common.log") local unit = {} +local ALARM = types.ALARM +local PRIO = types.ALARM_PRIORITY +local ALARM_STATE = types.ALARM_STATE + local TRI_FAIL = types.TRI_FAIL local DUMPING_MODE = types.DUMPING_MODE +local FLOW_STABILITY_DELAY_MS = 15000 + local DT_KEYS = { ReactorTemp = "RTP", ReactorFuel = "RFL", @@ -21,6 +27,32 @@ local DT_KEYS = { TurbinePower = "TPR" } +---@alias ALARM_INT_STATE integer +local AISTATE = { + INACTIVE = 0, + TRIPPING = 1, + TRIPPED = 2, + ACKED = 3, + RING_BACK = 4, + RING_BACK_TRIPPING = 5 +} + +local aistate_string = { + "INACTIVE", + "TRIPPING", + "TRIPPED", + "ACKED", + "RING_BACK", + "RING_BACK_TRIPPING" +} + +---@class alarm_def +---@field state ALARM_INT_STATE internal alarm state +---@field trip_time integer time (ms) when first tripped +---@field hold_time integer time (s) to hold before tripping +---@field id ALARM alarm ID +---@field tier integer alarm urgency tier (0 = highest) + -- create a new reactor unit ---@param for_reactor integer reactor unit number ---@param num_boilers integer number of boilers expected @@ -30,12 +62,49 @@ function unit.new(for_reactor, num_boilers, num_turbines) r_id = for_reactor, plc_s = nil, ---@class plc_session_struct plc_i = nil, ---@class plc_session - counts = { boilers = num_boilers, turbines = num_turbines }, turbines = {}, boilers = {}, redstone = {}, deltas = {}, last_heartbeat = 0, + -- logic for alarms + had_reactor = false, + start_time = 0, + plc_cache = { + ok = false, + rps_trip = false, + rps_status = {}, ---@type rps_status + damage = 0, + temp = 0, + waste = 0 + }, + ---@class alarm_monitors + alarms = { + -- reactor lost under the condition of meltdown imminent + ContainmentBreach = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ContainmentBreach, tier = PRIO.CRITICAL }, + -- radiation monitor alarm for this unit + ContainmentRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ContainmentRadiation, tier = PRIO.CRITICAL }, + -- reactor offline after being online + ReactorLost = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorLost, tier = PRIO.URGENT }, + -- damage >100% + CriticalDamage = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.CriticalDamage, tier = PRIO.CRITICAL }, + -- reactor damage increasing + ReactorDamage = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorDamage, tier = PRIO.EMERGENCY }, + -- reactor >1200K + ReactorOverTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorOverTemp, tier = PRIO.URGENT }, + -- reactor >1100K + ReactorHighTemp = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighTemp, tier = PRIO.TIMELY }, + -- waste = 100% + ReactorWasteLeak = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.ReactorWasteLeak, tier = PRIO.EMERGENCY }, + -- waste >85% + ReactorHighWaste = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighWaste, tier = PRIO.TIMELY }, + -- RPS trip occured + RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.RPSTransient, tier = PRIO.URGENT }, + -- BoilRateMismatch, CoolantFeedMismatch, SteamFeedMismatch, MaxWaterReturnFeed + RCSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 5, id = ALARM.RCSTransient, tier = PRIO.TIMELY }, + -- "It's just a routine turbin' trip!" -Bill Gibson + TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.TurbineTrip, tier = PRIO.URGENT } + }, db = { ---@class annunciator annunciator = { @@ -47,6 +116,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) AutoReactorSCRAM = false, RCPTrip = false, RCSFlowLow = false, + CoolantLevelLow = false, ReactorTempHigh = false, ReactorHighDeltaT = false, FuelInputRateLow = false, @@ -55,6 +125,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- boiler BoilerOnline = {}, HeatingRateLow = {}, + WaterLevelLow = {}, BoilRateMismatch = false, CoolantFeedMismatch = false, -- turbine @@ -64,6 +135,21 @@ function unit.new(for_reactor, num_boilers, num_turbines) SteamDumpOpen = {}, TurbineOverSpeed = {}, TurbineTrip = {} + }, + ---@class alarms + alarm_states = { + 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 } } } @@ -125,6 +211,92 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end + -- update an alarm state given conditions + ---@param tripped boolean if the alarm condition is still active + ---@param alarm alarm_def alarm table + local function _update_alarm_state(tripped, alarm) + local int_state = alarm.state + local ext_state = self.db.alarm_states[alarm.id] + + -- alarm inactive + if int_state == AISTATE.INACTIVE then + if tripped then + alarm.trip_time = util.time_ms() + if alarm.hold_time > 0 then + alarm.state = AISTATE.TRIPPING + self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE + else + alarm.state = AISTATE.TRIPPED + self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED + log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ", + types.alarm_prio_string[alarm.tier + 1],"]")) + end + else + alarm.trip_time = util.time_ms() + self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE + end + -- alarm condition met, but not yet for required hold time + elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then + if tripped then + local elapsed = util.time_ms() - alarm.trip_time + if elapsed > (alarm.hold_time * 1000) then + alarm.state = AISTATE.TRIPPED + self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED + log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ", + types.alarm_prio_string[alarm.tier + 1],"]")) + end + elseif int_state == AISTATE.RING_BACK_TRIPPING then + alarm.trip_time = 0 + alarm.state = AISTATE.RING_BACK + self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK + else + alarm.trip_time = 0 + alarm.state = AISTATE.INACTIVE + self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE + end + -- alarm tripped and alarming + elseif int_state == AISTATE.TRIPPED then + if tripped then + if ext_state == ALARM_STATE.ACKED then + -- was acked by coordinator + alarm.state = AISTATE.ACKED + end + else + alarm.state = AISTATE.RING_BACK + self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK + end + -- alarm acknowledged but still tripped + elseif int_state == AISTATE.ACKED then + if not tripped then + alarm.state = AISTATE.RING_BACK + self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK + end + -- alarm no longer tripped, operator must reset to clear + elseif int_state == AISTATE.RING_BACK then + if tripped then + alarm.trip_time = util.time_ms() + if alarm.hold_time > 0 then + alarm.state = AISTATE.RING_BACK_TRIPPING + else + alarm.state = AISTATE.TRIPPED + self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED + end + elseif ext_state == ALARM_STATE.INACTIVE then + -- was reset by coordinator + alarm.state = AISTATE.INACTIVE + alarm.trip_time = 0 + end + else + log.error(util.c("invalid alarm state for unit ", self.r_id, " alarm ", alarm.id), true) + end + + -- check for state change + if alarm.state ~= int_state then + local change_str = util.c(aistate_string[int_state + 1], " -> ", aistate_string[alarm.state + 1]) + log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): ", change_str)) + end + end + -- update all delta computations local function _dt__compute_all() if self.plc_s ~= nil then @@ -181,6 +353,21 @@ function unit.new(for_reactor, num_boilers, num_turbines) if self.plc_s ~= nil then local plc_db = self.plc_i.get_db() + -- record reactor start time (some alarms are delayed during reactor heatup) + if self.start_time == 0 and plc_db.mek_status.status then + self.start_time = util.time_ms() + elseif not plc_db.mek_status.status then + self.start_time = 0 + end + + -- record reactor stats + self.plc_cache.ok = not (plc_db.rps_status.fault or plc_db.rps_status.sys_fail or plc_db.rps_status.force_dis) + self.plc_cache.rps_trip = plc_db.rps_tripped + self.plc_cache.rps_status = plc_db.rps_status + self.plc_cache.damage = plc_db.mek_status.damage + self.plc_cache.temp = plc_db.mek_status.temp + self.plc_cache.waste = plc_db.mek_status.waste_fill + -- heartbeat blink about every second if self.last_heartbeat + 1000 < plc_db.last_status_update then self.db.annunciator.PLCHeartbeat = not self.db.annunciator.PLCHeartbeat @@ -201,9 +388,11 @@ function unit.new(for_reactor, num_boilers, num_turbines) self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and plc_db.mek_status.burn_rate > 40 -- if no boilers, use reactor heating rate to check for boil rate mismatch - if self.counts.boilers == 0 then + if num_boilers == 0 then total_boil_rate = plc_db.mek_status.heating_rate end + else + self.plc_cache.ok = false end ------------- @@ -211,13 +400,13 @@ function unit.new(for_reactor, num_boilers, num_turbines) ------------- -- clear boiler online flags - for i = 1, self.counts.boilers do self.db.annunciator.BoilerOnline[i] = false end + for i = 1, num_boilers do self.db.annunciator.BoilerOnline[i] = false end -- aggregated statistics local boiler_steam_dt_sum = 0.0 local boiler_water_dt_sum = 0.0 - if self.counts.boilers > 0 then + if num_boilers > 0 then -- go through boilers for stats and online for i = 1, #self.boilers do local session = self.boilers[i] ---@type unit_session @@ -259,7 +448,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- check coolant feed mismatch if using boilers, otherwise calculate with reactor local cfmismatch = false - if self.counts.boilers > 0 then + if num_boilers > 0 then for i = 1, #self.boilers do local boiler = self.boilers[i] ---@type unit_session local idx = boiler.get_device_idx() @@ -290,7 +479,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -------------- -- clear turbine online flags - for i = 1, self.counts.turbines do self.db.annunciator.TurbineOnline[i] = false end + for i = 1, num_turbines do self.db.annunciator.TurbineOnline[i] = false end -- aggregated statistics local total_flow_rate = 0 @@ -359,6 +548,75 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end + -- evaluate alarm conditions + local function _update_alarms() + local annunc = self.db.annunciator + local plc_cache = self.plc_cache + + -- Containment Breach + -- lost plc with critical damage (rip plc, you will be missed) + _update_alarm_state((not plc_cache.ok) and (plc_cache.damage > 99), self.alarms.ContainmentBreach) + + -- Containment Radiation + ---@todo containment radiation alarm + _update_alarm_state(false, self.alarms.ContainmentRadiation) + + -- Reactor Lost + _update_alarm_state(self.had_reactor and self.plc_s == nil, self.alarms.ReactorLost) + + -- Critical Damage + _update_alarm_state(plc_cache.damage >= 100, self.alarms.CriticalDamage) + + -- Reactor Damage + _update_alarm_state(plc_cache.damage > 0, self.alarms.ReactorDamage) + + -- Over-Temperature + _update_alarm_state(plc_cache.temp >= 1200, self.alarms.ReactorOverTemp) + + -- High Temperature + _update_alarm_state(plc_cache.temp > 1150, self.alarms.ReactorHighTemp) + + -- Waste Leak + _update_alarm_state(plc_cache.waste >= 0.99, self.alarms.ReactorWasteLeak) + + -- High Waste + _update_alarm_state(plc_cache.waste > 0.50, self.alarms.ReactorHighWaste) + + -- RPS Transient (excludes timeouts and manual trips) + local rps_alarm = false + if plc_cache.rps_status.manual ~= nil then + if plc_cache.rps_trip then + for key, val in pairs(plc_cache.rps_status) do + if key ~= "manual" and key ~= "timeout" then + rps_alarm = rps_alarm or val + end + end + end + end + + _update_alarm_state(rps_alarm, self.alarms.RPSTransient) + + -- RCS Transient + local any_low = false + local any_over = false + for i = 1, #annunc.WaterLevelLow do any_low = any_low or annunc.WaterLevelLow[i] end + for i = 1, #annunc.TurbineOverSpeed do any_over = any_over or annunc.TurbineOverSpeed[i] end + + local rcs_trans = any_low or any_over or annunc.RCPTrip or annunc.RCSFlowLow or annunc.MaxWaterReturnFeed + + -- flow is ramping up right after reactor start, annunciator indicators for these states may not indicate a real issue + if util.time_ms() - self.start_time > FLOW_STABILITY_DELAY_MS then + rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch + end + + _update_alarm_state(rcs_trans, self.alarms.RCSTransient) + + -- Turbine Trip + local any_trip = false + for i = 1, #annunc.TurbineTrip do any_trip = any_trip or annunc.TurbineTrip[i] end + _update_alarm_state(any_trip, self.alarms.TurbineTrip) + end + -- unlink disconnected units ---@param sessions table local function _unlink_disconnected_units(sessions) @@ -372,6 +630,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- link the PLC ---@param plc_session plc_session_struct function public.link_plc_session(plc_session) + self.had_reactor = true self.plc_s = plc_session self.plc_i = plc_session.instance @@ -443,6 +702,7 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- unlink PLC if session was closed if self.plc_s ~= nil and not self.plc_s.open then self.plc_s = nil + self.plc_i = nil end -- unlink RTU unit sessions if they are closed @@ -451,6 +711,36 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- update annunciator logic _update_annunciator() + + -- update alarm status + _update_alarms() + end + + -- ACK/RESET ALARMS -- + + -- acknowledge all alarms (if possible) + function public.ack_all() + for i = 1, #self.db.alarm_states do + if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then + self.db.alarm_states[i] = ALARM_STATE.ACKED + end + end + end + + -- acknowledge an alarm (if possible) + ---@param id ALARM alarm ID + function public.ack_alarm(id) + if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.TRIPPED) then + self.db.alarm_states[id] = ALARM_STATE.ACKED + end + end + + -- reset an alarm (if possible) + ---@param id ALARM alarm ID + function public.reset_alarm(id) + if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.RING_BACK) then + self.db.alarm_states[id] = ALARM_STATE.INACTIVE + end end -- READ STATES/PROPERTIES -- @@ -526,6 +816,9 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- get the annunciator status function public.get_annunciator() return self.db.annunciator end + -- get the alarm states + function public.get_alarms() return self.db.alarm_states end + -- get the reactor ID function public.get_id() return self.r_id end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 6a5db92..5fa14de 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.7.10" +local SUPERVISOR_VERSION = "beta-v0.8.0" local print = util.print local println = util.println