#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

This commit is contained in:
Mikayla Fischler 2022-11-26 16:18:31 -05:00
parent f68c38ccee
commit d4ae18eee7
16 changed files with 824 additions and 160 deletions

View File

@ -421,6 +421,8 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa
unit.set_burn_ack(ack) unit.set_burn_ack(ack)
elseif cmd == CRDN_COMMANDS.SET_WASTE then elseif cmd == CRDN_COMMANDS.SET_WASTE then
unit.set_waste_ack(ack) unit.set_waste_ack(ack)
elseif cmd == CRDN_COMMANDS.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
else else
log.debug(util.c("received command ack with unknown command ", cmd)) log.debug(util.c("received command ack with unknown command ", cmd))
end end
@ -474,6 +476,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa
else else
log.debug("supervisor connection denied") log.debug("supervisor connection denied")
end 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 else
log.debug("SCADA_MGMT establish packet length mismatch") log.debug("SCADA_MGMT establish packet length mismatch")
end end

View File

@ -1,6 +1,7 @@
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 util = require("scada-common.util") local util = require("scada-common.util")
local CRDN_COMMANDS = comms.CRDN_COMMANDS local CRDN_COMMANDS = comms.CRDN_COMMANDS
@ -22,9 +23,19 @@ function iocontrol.init(conf, comms)
io.units = {} io.units = {}
for i = 1, conf.num_units do 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 ---@class ioctl_entry
local entry = { local entry = {
unit_id = i, ---@type integer unit_id = i, ---@type integer
initialized = false, initialized = false,
num_boilers = 0, num_boilers = 0,
@ -37,17 +48,36 @@ function iocontrol.init(conf, comms)
start = function () end, start = function () end,
scram = function () end, scram = function () end,
reset_rps = function () end, reset_rps = function () end,
set_burn = function (rate) end, ack_alarms = function () end,
set_waste = function (mode) end, set_burn = function (rate) end, ---@param rate number
set_waste = function (mode) end, ---@param mode integer
start_ack = function (success) end, start_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, scram_ack = function (success) end, ---@param success boolean
reset_rps_ack = function (success) end, reset_rps_ack = function (success) end, ---@param success boolean
set_burn_ack = function (success) end, ack_alarms_ack = function (success) end,---@param success boolean
set_waste_ack = function (success) end, 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_ps = psil.create(),
reactor_data = {}, ---@type reactor_db reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {}, boiler_ps_tbl = {},
boiler_data_tbl = {}, boiler_data_tbl = {},
@ -73,6 +103,11 @@ function iocontrol.init(conf, comms)
log.debug(util.c("UNIT[", i, "]: RESET_RPS")) log.debug(util.c("UNIT[", i, "]: RESET_RPS"))
end 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) function entry.set_burn(rate)
comms.send_command(CRDN_COMMANDS.SET_BURN, i, rate) comms.send_command(CRDN_COMMANDS.SET_BURN, i, rate)
log.debug(util.c("UNIT[", i, "]: SET_BURN = ", rate)) log.debug(util.c("UNIT[", i, "]: SET_BURN = ", rate))
@ -167,7 +202,10 @@ end
---@param statuses table ---@param statuses table
---@return boolean valid ---@return boolean valid
function iocontrol.update_statuses(statuses) 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") log.error("number of provided unit statuses does not match expected number of units")
return false return false
else else
@ -175,6 +213,11 @@ function iocontrol.update_statuses(statuses)
local unit = io.units[i] ---@type ioctl_entry local unit = io.units[i] ---@type ioctl_entry
local status = statuses[i] 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 -- reactor PLC status
local reactor_status = status[1] local reactor_status = status[1]
@ -253,7 +296,7 @@ function iocontrol.update_statuses(statuses)
end end
unit.reactor_ps.publish("TurbineTrip", any) 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 -- split up array for all boilers
for id = 1, #val do for id = 1, #val do
unit.boiler_ps_tbl[id].publish(key, val[id]) unit.boiler_ps_tbl[id].publish(key, val[id])
@ -272,9 +315,25 @@ function iocontrol.update_statuses(statuses)
end end
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 -- RTU statuses
local rtu_statuses = status[3] local rtu_statuses = status[4]
if type(rtu_statuses) == "table" then if type(rtu_statuses) == "table" then
if type(rtu_statuses.boilers) == "table" then if type(rtu_statuses.boilers) == "table" then

View File

@ -17,7 +17,7 @@ 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 COORDINATOR_VERSION = "alpha-v0.6.17" local COORDINATOR_VERSION = "alpha-v0.7.0"
local print = util.print local print = util.print
local println = util.println local println = util.println

View File

@ -2,8 +2,6 @@
-- Reactor Unit SCADA Coordinator GUI -- Reactor Unit SCADA Coordinator GUI
-- --
local tcallbackdsp = require("scada-common.tcallbackdsp")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
@ -12,8 +10,8 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox") 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 CoreMap = require("graphics.elements.indicators.coremap")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
@ -30,6 +28,29 @@ local cpair = core.graphics.cpair
local period = core.flasher.PERIOD 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 -- create a unit view
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer ---@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} 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 hzd_fg_bg = cpair(colors.white, colors.gray)
local lu_cpair = cpair(colors.gray, colors.gray) local lu_cpair = cpair(colors.gray, colors.gray)
-----------------------------
-- main stats and core map -- -- main stats and core map --
-----------------------------
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18} local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
r_ps.subscribe("temp", core_map.update) 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} 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() main.line_break()
-----------------
-- annunciator -- -- 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 -- connectivity/basic state
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)} 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 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)} local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
---@todo auto control as info sent here ---@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("PLCOnline", plc_online.update)
r_ps.subscribe("PLCHeartbeat", plc_hbeat.update) r_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
@ -103,23 +128,25 @@ local function init(parent, id)
annunciator.line_break() 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_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_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_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_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_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_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_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_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_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("ReactorSCRAM", r_scram.update)
r_ps.subscribe("ManualReactorSCRAM", r_mscrm.update) r_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
r_ps.subscribe("AutoReactorSCRAM", r_ascrm.update) r_ps.subscribe("AutoReactorSCRAM", r_ascrm.update)
r_ps.subscribe("RCPTrip", r_rtrip.update) r_ps.subscribe("RCPTrip", r_rtrip.update)
r_ps.subscribe("RCSFlowLow", r_cflow.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("ReactorTempHigh", r_temp.update)
r_ps.subscribe("ReactorHighDeltaT", r_rhdt.update) r_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
r_ps.subscribe("FuelInputRateLow", r_firl.update) r_ps.subscribe("FuelInputRateLow", r_firl.update)
@ -128,14 +155,24 @@ local function init(parent, id)
annunciator.line_break() 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_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_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_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_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_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_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_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} 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() 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_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_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)} 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() annunciator.line_break()
-- machine-specific indicators -- boiler annunciator panel(s)
local tag_y = 1
if unit.num_boilers > 0 then 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)} local b1_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
b_ps[1].subscribe("HeatingRateLow", b1_hr.update) 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 end
if unit.num_boilers > 1 then 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)} local b2_hr = IndicatorLight{parent=annunciator,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
b_ps[2].subscribe("HeatingRateLow", b2_hr.update) 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 end
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
@ -185,7 +242,14 @@ local function init(parent, id)
annunciator.line_break() annunciator.line_break()
end 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} 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) 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)} local t1_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update) 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} 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) t_ps[1].subscribe("TurbineTrip", t1_trp.update)
TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)}
main.line_break()
annunciator.line_break()
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)}
@ -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)} local t2_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update) 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} 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) t_ps[2].subscribe("TurbineTrip", t2_trp.update)
TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)}
main.line_break()
annunciator.line_break()
end end
if unit.num_turbines > 2 then 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)} local t3_tos = IndicatorLight{parent=annunciator,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update) 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} 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) t_ps[3].subscribe("TurbineTrip", t3_trp.update)
TextBox{parent=main,x=34,y=tag_y,text="\x95",width=1,height=1,fg_bg=cpair(colors.lightGray, colors.cyan)}
annunciator.line_break()
end end
annunciator.line_break()
---@todo radiation monitor ---@todo radiation monitor
IndicatorLight{parent=annunciator,label="Radiation Monitor",colors=cpair(colors.green,colors.gray)} 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 -- -- 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_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)} 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 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 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 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 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 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 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.start_ack = start.on_response
unit.scram_ack = scram.on_response unit.scram_ack = scram.on_response
unit.reset_rps_ack = reset.on_response unit.reset_rps_ack = reset.on_response
unit.ack_alarms_ack = ack_a.on_response
local function start_button_en_check() local function start_button_en_check()
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then 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", start_button_en_check)
r_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) r_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
local opts = { TextBox{parent=main,x=2,y=30,text="Idle",width=29,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
{
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)
}
}
---@todo waste selection local waste_sel = Div{parent=main,x=2,y=50,width=29,height=2,fg_bg=cpair(colors.black, colors.white)}
local waste_sel = Div{parent=main,x=2,y=48,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} 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 return main
end end

View File

@ -26,6 +26,7 @@ local element = {}
---|push_button_args ---|push_button_args
---|spinbox_args ---|spinbox_args
---|switch_button_args ---|switch_button_args
---|alarm_indicator_light
---|core_map_args ---|core_map_args
---|data_indicator_args ---|data_indicator_args
---|hbar_args ---|hbar_args
@ -302,6 +303,18 @@ function element.new(args)
---@return cpair fg_bg ---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end 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 -- get element width
---@return integer width ---@return integer width
function public.width() function public.width()

View File

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

View File

@ -1,6 +1,9 @@
-- Tri-State Indicator Light Graphics Element -- Tri-State Indicator Light Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element") local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class tristate_indicator_light_args ---@class tristate_indicator_light_args
---@field label string indicator label ---@field label string indicator label
@ -8,13 +11,15 @@ local element = require("graphics.element")
---@field c2 color color for state 2 ---@field c2 color color for state 2
---@field c3 color color for state 3 ---@field c3 color color for state 3
---@field min_label_width? integer label length if omitted ---@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 parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new indicator light -- new tri-state indicator light
---@param args tristate_indicator_light_args ---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tristate_indicator_light(args) 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.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") 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 -- single line
args.height = 1 args.height = 1
-- determine width -- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- flasher state
local flash_on = true
-- blit translations -- blit translations
local c1 = colors.toBlit(args.c1) local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2) local c2 = colors.toBlit(args.c2)
@ -37,12 +49,45 @@ local function tristate_indicator_light(args)
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) 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 -- on state change
---@param new_state integer indicator state ---@param new_state integer indicator state
function e.on_update(new_state) function e.on_update(new_state)
local was_off = e.value <= 1
e.value = new_state e.value = new_state
e.window.setCursorPos(1, 1) 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) e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
elseif new_state == 3 then elseif new_state == 3 then
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) 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 function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light -- write label and initial indicator light
e.on_update(0) e.on_update(1)
e.window.write(args.label) e.window.write(args.label)
return e.get() return e.get()

View File

@ -14,7 +14,7 @@ local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc") local plc = require("reactor-plc.plc")
local threads = require("reactor-plc.threads") 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 print = util.print
local println = util.println local println = util.println

View File

@ -412,7 +412,7 @@ function rtu.comms(version, modem, local_port, server_port, conn_watchdog)
else else
-- establish denied -- establish denied
public.unlink(rtu_state) public.unlink(rtu_state)
println_ts("supervisor connection") println_ts("supervisor connection denied")
log.warning("supervisor connection denied by remote host") log.warning("supervisor connection denied by remote host")
end end
else else

View File

@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_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 local rtu_t = types.rtu_t

View File

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

View File

@ -12,7 +12,7 @@ local rtu_t = types.rtu_t
local insert = table.insert local insert = table.insert
comms.version = "1.0.0" comms.version = "1.0.1"
---@alias PROTOCOLS integer ---@alias PROTOCOLS integer
local PROTOCOLS = { local PROTOCOLS = {
@ -74,7 +74,10 @@ local CRDN_COMMANDS = {
START = 1, -- start the reactor START = 1, -- start the reactor
RESET_RPS = 2, -- reset the RPS RESET_RPS = 2, -- reset the RPS
SET_BURN = 3, -- set the burn rate 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 ---@alias CAPI_TYPES integer

View File

@ -35,6 +35,76 @@ types.TRI_FAIL = {
FULL = 2 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 -- -- STRING TYPES --
---@alias os_event ---@alias os_event

View File

@ -129,7 +129,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units)
for i = 1, #self.units do for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit 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 end
_send(SCADA_CRDN_TYPES.UNIT_STATUSES, status) _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 -- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #self.units then 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 if cmd == CRDN_COMMANDS.START then
self.out_q.push_data(SV_Q_DATA.START, data) self.out_q.push_data(SV_Q_DATA.START, data)
elseif cmd == CRDN_COMMANDS.SCRAM then elseif cmd == CRDN_COMMANDS.SCRAM then
@ -209,6 +211,21 @@ function coordinator.new_session(id, in_queue, out_queue, facility_units)
else else
log.debug(log_header .. "CRDN command unit set waste missing option") log.debug(log_header .. "CRDN command unit set waste missing option")
end 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 else
log.debug(log_header .. "CRDN command unknown") log.debug(log_header .. "CRDN command unknown")
end end

View File

@ -4,9 +4,15 @@ local log = require("scada-common.log")
local unit = {} local unit = {}
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL local TRI_FAIL = types.TRI_FAIL
local DUMPING_MODE = types.DUMPING_MODE local DUMPING_MODE = types.DUMPING_MODE
local FLOW_STABILITY_DELAY_MS = 15000
local DT_KEYS = { local DT_KEYS = {
ReactorTemp = "RTP", ReactorTemp = "RTP",
ReactorFuel = "RFL", ReactorFuel = "RFL",
@ -21,6 +27,32 @@ local DT_KEYS = {
TurbinePower = "TPR" 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 -- create a new reactor unit
---@param for_reactor integer reactor unit number ---@param for_reactor integer reactor unit number
---@param num_boilers integer number of boilers expected ---@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, r_id = for_reactor,
plc_s = nil, ---@class plc_session_struct plc_s = nil, ---@class plc_session_struct
plc_i = nil, ---@class plc_session plc_i = nil, ---@class plc_session
counts = { boilers = num_boilers, turbines = num_turbines },
turbines = {}, turbines = {},
boilers = {}, boilers = {},
redstone = {}, redstone = {},
deltas = {}, deltas = {},
last_heartbeat = 0, 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 = { db = {
---@class annunciator ---@class annunciator
annunciator = { annunciator = {
@ -47,6 +116,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
AutoReactorSCRAM = false, AutoReactorSCRAM = false,
RCPTrip = false, RCPTrip = false,
RCSFlowLow = false, RCSFlowLow = false,
CoolantLevelLow = false,
ReactorTempHigh = false, ReactorTempHigh = false,
ReactorHighDeltaT = false, ReactorHighDeltaT = false,
FuelInputRateLow = false, FuelInputRateLow = false,
@ -55,6 +125,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- boiler -- boiler
BoilerOnline = {}, BoilerOnline = {},
HeatingRateLow = {}, HeatingRateLow = {},
WaterLevelLow = {},
BoilRateMismatch = false, BoilRateMismatch = false,
CoolantFeedMismatch = false, CoolantFeedMismatch = false,
-- turbine -- turbine
@ -64,6 +135,21 @@ function unit.new(for_reactor, num_boilers, num_turbines)
SteamDumpOpen = {}, SteamDumpOpen = {},
TurbineOverSpeed = {}, TurbineOverSpeed = {},
TurbineTrip = {} 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
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 -- update all delta computations
local function _dt__compute_all() local function _dt__compute_all()
if self.plc_s ~= nil then 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 if self.plc_s ~= nil then
local plc_db = self.plc_i.get_db() 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 -- heartbeat blink about every second
if self.last_heartbeat + 1000 < plc_db.last_status_update then if self.last_heartbeat + 1000 < plc_db.last_status_update then
self.db.annunciator.PLCHeartbeat = not self.db.annunciator.PLCHeartbeat 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 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 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 total_boil_rate = plc_db.mek_status.heating_rate
end end
else
self.plc_cache.ok = false
end end
------------- -------------
@ -211,13 +400,13 @@ function unit.new(for_reactor, num_boilers, num_turbines)
------------- -------------
-- clear boiler online flags -- 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 -- aggregated statistics
local boiler_steam_dt_sum = 0.0 local boiler_steam_dt_sum = 0.0
local boiler_water_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 -- go through boilers for stats and online
for i = 1, #self.boilers do for i = 1, #self.boilers do
local session = self.boilers[i] ---@type unit_session 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 -- check coolant feed mismatch if using boilers, otherwise calculate with reactor
local cfmismatch = false local cfmismatch = false
if self.counts.boilers > 0 then if num_boilers > 0 then
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
local idx = boiler.get_device_idx() local idx = boiler.get_device_idx()
@ -290,7 +479,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-------------- --------------
-- clear turbine online flags -- 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 -- aggregated statistics
local total_flow_rate = 0 local total_flow_rate = 0
@ -359,6 +548,75 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end end
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 -- unlink disconnected units
---@param sessions table ---@param sessions table
local function _unlink_disconnected_units(sessions) local function _unlink_disconnected_units(sessions)
@ -372,6 +630,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- link the PLC -- link the PLC
---@param plc_session plc_session_struct ---@param plc_session plc_session_struct
function public.link_plc_session(plc_session) function public.link_plc_session(plc_session)
self.had_reactor = true
self.plc_s = plc_session self.plc_s = plc_session
self.plc_i = plc_session.instance 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 -- unlink PLC if session was closed
if self.plc_s ~= nil and not self.plc_s.open then if self.plc_s ~= nil and not self.plc_s.open then
self.plc_s = nil self.plc_s = nil
self.plc_i = nil
end end
-- unlink RTU unit sessions if they are closed -- 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 logic
_update_annunciator() _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 end
-- READ STATES/PROPERTIES -- -- READ STATES/PROPERTIES --
@ -526,6 +816,9 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- get the annunciator status -- get the annunciator status
function public.get_annunciator() return self.db.annunciator end 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 -- get the reactor ID
function public.get_id() return self.r_id end function public.get_id() return self.r_id end

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.7.10" local SUPERVISOR_VERSION = "beta-v0.8.0"
local print = util.print local print = util.print
local println = util.println local println = util.println