Merge pull request #199 from MikaylaFischler/latest

2023.04.09 Release
This commit is contained in:
Mikayla 2023-04-09 18:04:01 -04:00 committed by GitHub
commit 04d73cdcd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1135 additions and 188 deletions

View File

@ -3,14 +3,14 @@
-- --
--[[ --[[
Copyright © 2023 Mikayla Fischler Copyright (c) 2023 Mikayla Fischler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the Software), to deal in the Software without restriction, associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE

View File

@ -683,23 +683,13 @@ function iocontrol.update_unit_statuses(statuses)
end end
for key, val in pairs(unit.annunciator) do for key, val in pairs(unit.annunciator) do
if key == "TurbineTrip" then if key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up turbine trip table for all turbines and a general OR combination
local trips = val
local any = false
for id = 1, #trips do
any = any or trips[id]
unit.turbine_ps_tbl[id].publish(key, trips[id])
end
unit.unit_ps.publish("TurbineTrip", any)
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])
end end
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" or
key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines -- split up array for all turbines
for id = 1, #val do for id = 1, #val do
unit.turbine_ps_tbl[id].publish(key, val[id]) unit.turbine_ps_tbl[id].publish(key, val[id])

View File

@ -171,15 +171,15 @@ end
function renderer.ui_ready() return engine.ui_ready end function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event -- handle a touch event
---@param event monitor_touch ---@param event mouse_interaction
function renderer.handle_touch(event) function renderer.handle_mouse(event)
if event.monitor == engine.monitors.primary_name then if event.monitor == engine.monitors.primary_name then
ui.main_layout.handle_touch(event) ui.main_layout.handle_mouse(event)
else else
for id, monitor in pairs(engine.monitors.unit_name_map) do for id, monitor in pairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then if event.monitor == monitor then
local layout = ui.unit_layouts[id] ---@type graphics_element local layout = ui.unit_layouts[id] ---@type graphics_element
layout.handle_touch(event) layout.handle_mouse(event)
end end
end end
end end

View File

@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local COORDINATOR_VERSION = "v0.12.2" local COORDINATOR_VERSION = "v0.12.5"
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -354,7 +354,7 @@ local function main()
end end
elseif event == "monitor_touch" then elseif event == "monitor_touch" then
-- handle a monitor touch event -- handle a monitor touch event
renderer.handle_touch(core.events.touch(param1, param2, param3)) renderer.handle_mouse(core.events.touch(param1, param2, param3))
elseif event == "speaker_audio_empty" then elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied -- handle speaker buffer emptied
sounder.continue() sounder.continue()

View File

@ -235,8 +235,8 @@ local function init(parent, id)
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22} TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22}
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23} local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
local rcs_annunc = Div{parent=rcs,width=27,height=23,x=2,y=1} local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=14,x=29,y=9} local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)} local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green} local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
@ -244,7 +244,6 @@ local function init(parent, id)
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)} local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)} local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
u_ps.subscribe("RCSFault", c_flt.update) u_ps.subscribe("RCSFault", c_flt.update)
u_ps.subscribe("EmergencyCoolant", c_emg.update) u_ps.subscribe("EmergencyCoolant", c_emg.update)
@ -252,12 +251,19 @@ local function init(parent, id)
u_ps.subscribe("BoilRateMismatch", c_brm.update) u_ps.subscribe("BoilRateMismatch", c_brm.update)
u_ps.subscribe("SteamFeedMismatch", c_sfm.update) u_ps.subscribe("SteamFeedMismatch", c_sfm.update)
u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update) u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
u_ps.subscribe("TurbineTrip", c_tbnt.update)
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
local function _add_space()
-- if we have some extra space, add padding
rcs_tags.line_break()
rcs_annunc.line_break() rcs_annunc.line_break()
end
-- boiler annunciator panel(s) -- boiler annunciator panel(s)
if available_space > 0 then _add_space() end
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)} local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
@ -268,6 +274,13 @@ local function init(parent, id)
b_ps[1].subscribe("HeatingRateLow", b1_hr.update) b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
end end
if unit.num_boilers > 1 then if unit.num_boilers > 1 then
-- note, can't (shouldn't for sure...) have 0 turbines
if (available_space > 2 and unit.num_turbines == 1) or
(available_space > 3 and unit.num_turbines == 2) or
(available_space > 4) then
_add_space()
end
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)} local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
b_ps[2].subscribe("WasterLevelLow", b2_wll.update) b_ps[2].subscribe("WasterLevelLow", b2_wll.update)
@ -279,14 +292,9 @@ local function init(parent, id)
-- turbine annunciator panels -- turbine annunciator panels
if unit.num_boilers == 0 then if available_space > 1 then _add_space() end
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
else
rcs_tags.line_break()
rcs_annunc.line_break()
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
end
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update) t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
@ -294,11 +302,19 @@ local function init(parent, id)
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} local t1_tos = IndicatorLight{parent=rcs_annunc,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=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
t_ps[1].subscribe("GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local t1_trp = IndicatorLight{parent=rcs_annunc,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)
if unit.num_turbines > 1 then if unit.num_turbines > 1 then
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
_add_space()
end
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update) t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
@ -307,12 +323,18 @@ local function init(parent, id)
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} local t2_tos = IndicatorLight{parent=rcs_annunc,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=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
t_ps[2].subscribe("GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local t2_trp = IndicatorLight{parent=rcs_annunc,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)
end end
if unit.num_turbines > 2 then if unit.num_turbines > 2 then
if available_space > 3 then _add_space() end
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update) t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
@ -321,6 +343,10 @@ local function init(parent, id)
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)} local t3_tos = IndicatorLight{parent=rcs_annunc,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=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
t_ps[3].subscribe("GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} local t3_trp = IndicatorLight{parent=rcs_annunc,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)

View File

@ -10,20 +10,76 @@ core.flasher = flasher
local events = {} local events = {}
---@class monitor_touch ---@enum click_type
events.click_type = {
VIRTUAL = 0,
LEFT_BUTTON = 1,
RIGHT_BUTTON = 2,
MID_BUTTON = 3
}
---@class mouse_interaction
---@field monitor string ---@field monitor string
---@field button integer
---@field x integer ---@field x integer
---@field y integer ---@field y integer
-- create a new touch event definition -- create a new monitor touch mouse interaction event
---@nodiscard ---@nodiscard
---@param monitor string ---@param monitor string
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@return monitor_touch ---@return mouse_interaction
function events.touch(monitor, x, y) function events.touch(monitor, x, y)
return { return {
monitor = monitor, monitor = monitor,
button = events.click_type.LEFT_BUTTON,
x = x,
y = y
}
end
-- create a new mouse click mouse interaction event
---@nodiscard
---@param button click_type
---@param x integer
---@param y integer
---@return mouse_interaction
function events.click(button, x, y)
return {
monitor = "terminal",
button = button,
x = x,
y = y
}
end
-- create a new transposed mouse interaction event using the event's monitor/button fields
---@nodiscard
---@param event mouse_interaction
---@param new_x integer
---@param new_y integer
---@return mouse_interaction
function events.mouse_transposed(event, new_x, new_y)
return {
monitor = event.monitor,
button = event.button,
x = new_x,
y = new_y
}
end
-- create a new generic mouse interaction event
---@nodiscard
---@param monitor string
---@param button click_type
---@param x integer
---@param y integer
---@return mouse_interaction
function events.mouse_generic(monitor, button, x, y)
return {
monitor = monitor,
button = button,
x = x, x = x,
y = y y = y
} }

View File

@ -32,6 +32,9 @@ local element = {}
---|data_indicator_args ---|data_indicator_args
---|hbar_args ---|hbar_args
---|icon_indicator_args ---|icon_indicator_args
---|indicator_led_args
---|indicator_led_pair_args
---|indicator_led_rgb_args
---|indicator_light_args ---|indicator_light_args
---|power_indicator_args ---|power_indicator_args
---|rad_indicator_args ---|rad_indicator_args
@ -100,7 +103,13 @@ function element.new(args)
else else
local w, h = self.p_window.getSize() local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1 protected.frame.x = args.x or 1
if args.parent ~= nil then
protected.frame.y = args.y or (next_y - offset_y)
else
protected.frame.y = args.y or next_y protected.frame.y = args.y or next_y
end
protected.frame.w = args.width or w protected.frame.w = args.width or w
protected.frame.h = args.height or h protected.frame.h = args.height or h
end end
@ -157,9 +166,9 @@ function element.new(args)
self.bounds.y2 = self.position.y + f.h - 1 self.bounds.y2 = self.position.y + f.h - 1
end end
-- handle a touch event -- handle a mouse event
---@param event table monitor_touch event ---@param event mouse_interaction mouse interaction event
function protected.handle_touch(event) function protected.handle_mouse(event)
end end
-- handle data value changes -- handle data value changes
@ -260,6 +269,11 @@ function element.new(args)
---@param child graphics_template ---@param child graphics_template
---@return integer|string key ---@return integer|string key
function public.__add_child(key, child) function public.__add_child(key, child)
-- offset first automatic placement
if self.next_y <= self.child_offset.y then
self.next_y = self.child_offset.y + 1
end
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y) child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
self.next_y = child.frame.y + child.frame.h self.next_y = child.frame.y + child.frame.h
@ -396,20 +410,20 @@ function element.new(args)
-- FUNCTION CALLBACKS -- -- FUNCTION CALLBACKS --
-- handle a monitor touch -- handle a monitor touch or mouse click
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse interaction event
function public.handle_touch(event) function public.handle_mouse(event)
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2 local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2 local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
if in_x and in_y then if in_x and in_y then
local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1) local event_T = core.events.mouse_transposed(event, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
-- handle the touch event, transformed into the window frame -- handle the touch event, transformed into the window frame
protected.handle_touch(event_T) protected.handle_mouse(event_T)
-- pass on touch event to children -- pass on touch event to children
for _, val in pairs(self.children) do val.handle_touch(event_T) end for _, val in pairs(self.children) do val.handle_mouse(event_T) end
end end
end end

View File

@ -140,10 +140,10 @@ local function hazard_button(args)
end end
end end
-- handle touch -- handle mouse interaction
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local ---@diagnostic disable-next-line: unused-local
function e.handle_touch(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
-- change text color to indicate clicked -- change text color to indicate clicked
e.window.setTextColor(args.accent) e.window.setTextColor(args.accent)
@ -178,7 +178,7 @@ local function hazard_button(args)
-- set the value (true simulates pressing the button) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
end end
-- show the button as disabled -- show the button as disabled

View File

@ -92,9 +92,10 @@ local function multi_button(args)
end end
end end
-- handle touch -- handle mouse interaction
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse event
function e.handle_touch(event) ---@diagnostic disable-next-line: unused-local
function e.handle_mouse(event)
-- determine what was pressed -- determine what was pressed
if e.enabled and event.y == 1 then if e.enabled and event.y == 1 then
for i = 1, #args.options do for i = 1, #args.options do

View File

@ -8,7 +8,7 @@ local element = require("graphics.element")
---@class push_button_args ---@class push_button_args
---@field text string button text ---@field text string button text
---@field callback function function to call on touch ---@field callback function function to call on touch
---@field min_width? integer text length + 2 if omitted ---@field min_width? integer text length if omitted
---@field active_fg_bg? cpair foreground/background colors when pressed ---@field active_fg_bg? cpair foreground/background colors when pressed
---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element ---@field parent graphics_element
@ -47,10 +47,10 @@ local function push_button(args)
e.window.write(args.text) e.window.write(args.text)
end end
-- handle touch -- handle mouse interaction
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local ---@diagnostic disable-next-line: unused-local
function e.handle_touch(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
if args.active_fg_bg ~= nil then if args.active_fg_bg ~= nil then
-- show as pressed -- show as pressed
@ -78,7 +78,7 @@ local function push_button(args)
-- set the value (true simulates pressing the button) -- set the value (true simulates pressing the button)
---@param val boolean new value ---@param val boolean new value
function e.set_value(val) function e.set_value(val)
if val then e.handle_touch(core.events.touch("", 1, 1)) end if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
end end
-- show butten as enabled -- show butten as enabled

View File

@ -79,9 +79,9 @@ local function radio_button(args)
end end
end end
-- handle touch -- handle mouse interaction
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse event
function e.handle_touch(event) function e.handle_mouse(event)
-- determine what was pressed -- determine what was pressed
if e.enabled then if e.enabled then
if args.options[event.y] ~= nil then if args.options[event.y] ~= nil then

View File

@ -127,9 +127,9 @@ local function spinbox(args)
-- init with the default value -- init with the default value
show_num() show_num()
-- handle touch -- handle mouse interaction
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse event
function e.handle_touch(event) function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow -- only handle if on an increment or decrement arrow
if e.enabled and event.x ~= dec_point_x then if e.enabled and event.x ~= dec_point_x then
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x) local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)

View File

@ -62,10 +62,10 @@ local function switch_button(args)
-- initial draw -- initial draw
draw_state() draw_state()
-- handle touch -- handle mouse interaction
---@param event monitor_touch monitor touch event ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local ---@diagnostic disable-next-line: unused-local
function e.handle_touch(event) function e.handle_mouse(event)
if e.enabled then if e.enabled then
-- toggle state -- toggle state
e.value = not e.value e.value = not e.value

View File

@ -0,0 +1,98 @@
-- Indicator "LED" Graphics Element
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class indicator_led_args
---@field label string indicator label
---@field colors cpair on/off colors (a/b respectively)
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash on true 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 LED
---@nodiscard
---@param args indicator_led_args
---@return graphics_element element, element_id id
local function indicator_led(args)
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.led: 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
-- 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
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
else
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end
flash_on = not flash_on
end
-- enable light or start flashing
local function enable()
if args.flash then
flash_on = true
flasher.start(flash_callback, args.period)
else
e.window.setCursorPos(1, 1)
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
end
end
-- disable light or stop flashing
local function disable()
if args.flash then
flash_on = false
flasher.stop(flash_callback)
end
e.window.setCursorPos(1, 1)
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
end
-- on state change
---@param new_state boolean indicator state
function e.on_update(new_state)
e.value = new_state
if new_state then enable() else disable() end
end
-- set indicator state
---@param val boolean indicator state
function e.set_value(val) e.on_update(val) end
-- write label and initial indicator light
e.on_update(false)
e.window.setCursorPos(3, 1)
e.window.write(args.label)
return e.get()
end
return indicator_led

View File

@ -0,0 +1,112 @@
-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b)
local util = require("scada-common.util")
local element = require("graphics.element")
local flasher = require("graphics.flasher")
---@class indicator_led_pair_args
---@field label string indicator label
---@field off color color for off
---@field c1 color color for #1 on
---@field c2 color color for #2 on
---@field min_label_width? integer label length if omitted
---@field flash? boolean whether to flash when on 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 dual LED indicator light
---@nodiscard
---@param args indicator_led_pair_args
---@return graphics_element element, element_id id
local function indicator_led_pair(args)
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
if args.flash then
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: 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 co = colors.toBlit(args.off)
local c1 = colors.toBlit(args.c1)
local c2 = colors.toBlit(args.c2)
-- 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("\x8c", c1, e.fg_bg.blit_bkg)
elseif e.value == 3 then
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
end
else
e.window.blit("\x8c", co, 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 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("\x8c", co, e.fg_bg.blit_bkg)
end
elseif new_state == 2 then
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
elseif new_state == 3 then
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
else
e.window.blit("\x8c", co, 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.setCursorPos(3, 1)
e.window.write(args.label)
return e.get()
end
return indicator_led_pair

View File

@ -0,0 +1,57 @@
-- Indicator RGB LED Graphics Element
local element = require("graphics.element")
---@class indicator_led_rgb_args
---@field label string indicator label
---@field colors table colors to use
---@field min_label_width? integer label length if omitted
---@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 RGB LED indicator light
---@nodiscard
---@param args indicator_led_rgb_args
---@return graphics_element element, element_id id
local function indicator_led_rgb(args)
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
-- single line
args.height = 1
-- determine width
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args)
-- init value for initial check in on_update
e.value = 1
-- on state change
---@param new_state integer indicator state
function e.on_update(new_state)
e.value = new_state
e.window.setCursorPos(1, 1)
if type(args.colors[new_state]) == "number" then
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), 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.setCursorPos(3, 1)
e.window.write(args.label)
return e.get()
end
return indicator_led_rgb

View File

@ -7,6 +7,7 @@ local element = require("graphics.element")
---@class rectangle_args ---@class rectangle_args
---@field border? graphics_border ---@field border? graphics_border
---@field thin? boolean true to use extra thin even borders ---@field thin? boolean true to use extra thin even borders
---@field even_inner? boolean true to make the inner area of a border even
---@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
@ -66,14 +67,27 @@ local function rectangle(args)
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w) local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
-- partial bars -- partial bars
local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width) local p_a, p_b, p_s
local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
local p_s = spaces
if args.thin == true then if args.thin == true then
if args.even_inner == true then
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93"
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e"
else
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94" p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85" p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
end
p_s = "\x95" .. util.spaces(inner_width) .. "\x95" p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
else
if args.even_inner == true then
p_a = util.strrep("\x83", inner_width + width_x2)
p_b = util.strrep("\x8f", inner_width + width_x2)
else
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
end
p_s = spaces
end end
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
@ -112,10 +126,13 @@ local function rectangle(args)
if args.thin == true then if args.thin == true then
e.window.blit(p_a, p_inv_bg, p_inv_fg) e.window.blit(p_a, p_inv_bg, p_inv_fg)
else else
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
if width_x2 % 3 == 1 then if width_x2 % 3 == 1 then
e.window.blit(p_b, p_inv_bg, p_inv_fg) e.window.blit(p_b, _fg, _bg)
elseif width_x2 % 3 == 2 then elseif width_x2 % 3 == 2 then
e.window.blit(p_a, p_inv_bg, p_inv_fg) e.window.blit(p_a, _fg, _bg)
else else
-- skip line -- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides) e.window.blit(spaces, blit_fg, blit_bg_sides)
@ -129,12 +146,19 @@ local function rectangle(args)
-- partial pixel fill -- partial pixel fill
if args.border.even and y == ((e.frame.h - border_width) + 1) then if args.border.even and y == ((e.frame.h - border_width) + 1) then
if args.thin == true then if args.thin == true then
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) if args.even_inner == true then
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
else else
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
end
else
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
if width_x2 % 3 == 1 then if width_x2 % 3 == 1 then
e.window.blit(p_a, p_inv_fg, blit_bg_top_bot) e.window.blit(p_a, _fg, _bg)
elseif width_x2 % 3 == 2 or (args.thin == true) then elseif width_x2 % 3 == 2 then
e.window.blit(p_b, p_inv_fg, blit_bg_top_bot) e.window.blit(p_b, _fg, _bg)
else else
-- skip line -- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides) e.window.blit(spaces, blit_fg, blit_bg_sides)

View File

@ -7,7 +7,7 @@ def list_files(path):
for (root, dirs, files) in os.walk(path): for (root, dirs, files) in os.walk(path):
for f in files: for f in files:
list.append(root[2:] + "/" + f) list.append((root[2:] + "/" + f).replace('\\','/'))
return list return list
@ -68,7 +68,7 @@ def make_manifest(size):
"pocket" : list_files("./pocket"), "pocket" : list_files("./pocket"),
}, },
"depends" : { "depends" : {
"reactor-plc" : [ "system", "common" ], "reactor-plc" : [ "system", "common", "graphics" ],
"rtu" : [ "system", "common" ], "rtu" : [ "system", "common" ],
"supervisor" : [ "system", "common" ], "supervisor" : [ "system", "common" ],
"coordinator" : [ "system", "common", "graphics" ], "coordinator" : [ "system", "common", "graphics" ],

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,10 @@ config.NETWORKED = true
-- unique reactor ID -- unique reactor ID
config.REACTOR_ID = 1 config.REACTOR_ID = 1
-- for offline mode, this redstone interface will turn off (open a valve)
-- when emergency coolant is needed due to low coolant
config.EMERGENCY_COOL = { side = "right", color = nil }
-- port to send packets TO server -- port to send packets TO server
config.SERVER_PORT = 16000 config.SERVER_PORT = 16000
-- port to listen to incoming packets FROM server -- port to listen to incoming packets FROM server

101
reactor-plc/databus.lua Normal file
View File

@ -0,0 +1,101 @@
--
-- Data Bus - Central Communication Linking for PLC Front Panel
--
local log = require("scada-common.log")
local psil = require("scada-common.psil")
local util = require("scada-common.util")
local databus = {}
local dbus_iface = {
ps = psil.create(),
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
}
-- call to toggle heartbeat signal
function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
-- link RPS command functions
---@param scram function reactor SCRAM function
---@param reset function RPS reset function
function databus.link_rps(scram, reset)
dbus_iface.rps_scram = scram
dbus_iface.rps_reset = reset
end
-- transmit a command to the RPS to SCRAM
function databus.rps_scram() dbus_iface.rps_scram() end
-- transmit a command to the RPS to reset
function databus.rps_reset() dbus_iface.rps_reset() end
-- transmit firmware versions across the bus
---@param plc_v string PLC version
---@param comms_v string comms version
function databus.tx_versions(plc_v, comms_v)
dbus_iface.ps.publish("version", plc_v)
dbus_iface.ps.publish("comms_version", comms_v)
end
-- transmit unit ID across the bus
---@param id integer unit ID
function databus.tx_id(id)
dbus_iface.ps.publish("unit_id", id)
end
-- transmit hardware status across the bus
---@param plc_state plc_state
function databus.tx_hw_status(plc_state)
dbus_iface.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
dbus_iface.ps.publish("has_modem", not plc_state.no_modem)
dbus_iface.ps.publish("degraded", plc_state.degraded)
dbus_iface.ps.publish("init_ok", plc_state.init_ok)
end
-- transmit thread (routine) statuses
---@param thread string thread name
---@param ok boolean thread state
function databus.tx_rt_status(thread, ok)
dbus_iface.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
---@param state integer
function databus.tx_link_state(state)
dbus_iface.ps.publish("link_state", state)
end
-- transmit reactor enable state across the bus
---@param active boolean reactor active
function databus.tx_reactor_state(active)
dbus_iface.ps.publish("reactor_active", active)
end
-- transmit RPS data across the bus
---@param tripped boolean RPS tripped
---@param status table RPS status
function databus.tx_rps(tripped, status)
dbus_iface.ps.publish("rps_scram", tripped)
dbus_iface.ps.publish("rps_damage", status[1])
dbus_iface.ps.publish("rps_high_temp", status[2])
dbus_iface.ps.publish("rps_low_ccool", status[3])
dbus_iface.ps.publish("rps_high_waste", status[4])
dbus_iface.ps.publish("rps_high_hcool", status[5])
dbus_iface.ps.publish("rps_no_fuel", status[6])
dbus_iface.ps.publish("rps_fault", status[7])
dbus_iface.ps.publish("rps_timeout", status[8])
dbus_iface.ps.publish("rps_manual", status[9])
dbus_iface.ps.publish("rps_automatic", status[10])
dbus_iface.ps.publish("rps_sysfail", status[11])
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
dbus_iface.ps.subscribe(field, func)
end
return databus

View File

@ -0,0 +1,124 @@
--
-- Main SCADA Coordinator GUI
--
local util = require("scada-common.util")
local databus = require("reactor-plc.databus")
local style = require("reactor-plc.panel.style")
local core = require("graphics.core")
local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local LED = require("graphics.elements.indicators.led")
local LEDPair = require("graphics.elements.indicators.ledpair")
local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
local border = core.graphics.border
-- create new main view
---@param monitor table main viewscreen
local function init(monitor)
local panel = DisplayBox{window=monitor,fg_bg=style.root}
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
databus.rx_field("unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
system.line_break()
databus.rx_field("init_ok", init_ok.update)
databus.rx_field("heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(5)
system.line_break()
databus.rx_field("reactor_dev_state", reactor.update)
databus.rx_field("has_modem", modem.update)
databus.rx_field("link_state", network.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)}
local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.green_off)}
local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)}
system.line_break()
databus.rx_field("routine__main", rt_main.update)
databus.rx_field("routine__rps", rt_rps.update)
databus.rx_field("routine__comms_tx", rt_cmtx.update)
databus.rx_field("routine__comms_rx", rt_cmrx.update)
databus.rx_field("routine__spctl", rt_sctl.update)
local status = Div{parent=panel,width=19,height=18,x=17,y=3}
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)}
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,y=2,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS}
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,y=5,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
databus.rx_field("reactor_active", active.update)
databus.rx_field("rps_scram", scram.update)
local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)}
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)}
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)}
local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)}
local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)}
rps.line_break()
local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)}
local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)}
rps.line_break()
local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)}
local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)}
rps.line_break()
local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)}
local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)}
databus.rx_field("rps_manual", rps_man.update)
databus.rx_field("rps_automatic", rps_auto.update)
databus.rx_field("rps_timeout", rps_tmo.update)
databus.rx_field("rps_fault", rps_flt.update)
databus.rx_field("rps_sysfail", rps_fail.update)
databus.rx_field("rps_damage", rps_dmg.update)
databus.rx_field("rps_high_temp", rps_tmp.update)
databus.rx_field("rps_no_fuel", rps_nof.update)
databus.rx_field("rps_high_waste", rps_wst.update)
databus.rx_field("rps_low_ccool", rps_ccl.update)
databus.rx_field("rps_high_hcool", rps_hcl.update)
return panel
end
return init

View File

@ -0,0 +1,41 @@
--
-- Graphics Style Options
--
local core = require("graphics.core")
local style = {}
local cpair = core.graphics.cpair
-- GLOBAL --
-- remap global colors
colors.ivory = colors.pink
colors.red_off = colors.brown
colors.yellow_off = colors.magenta
colors.green_off = colors.lime
style.root = cpair(colors.black, colors.ivory)
style.header = cpair(colors.black, colors.lightGray)
style.colors = {
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
{ c = colors.orange, hex = 0xffb659 },
{ c = colors.yellow, hex = 0xf9fb53 },
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
{ c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff },
{ c = colors.purple, hex = 0xb156ee },
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca },
{ c = colors.lightGray, hex = 0xb1b8b3 },
{ c = colors.gray, hex = 0x575757 },
-- { c = colors.black, hex = 0x191919 },
{ c = colors.brown, hex = 0x672223 } -- RED OFF
}
return style

View File

@ -1,7 +1,9 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local const = require("scada-common.constants") local const = require("scada-common.constants")
local databus = require("reactor-plc.databus")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -18,11 +20,6 @@ local AUTO_ACK = comms.PLC_AUTO_ACK
local RPS_LIMITS = const.RPS_LIMITS local RPS_LIMITS = const.RPS_LIMITS
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
-- I sure hope the devs don't change this error message, not that it would have safety implications -- I sure hope the devs don't change this error message, not that it would have safety implications
-- I wish they didn't change it to be like this -- I wish they didn't change it to be like this
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
@ -34,7 +31,8 @@ local PCALL_START_MSG = "pcall: Reactor is already active."
---@nodiscard ---@nodiscard
---@param reactor table ---@param reactor table
---@param is_formed boolean ---@param is_formed boolean
function plc.rps_init(reactor, is_formed) ---@param emer_cool nil|table emergency coolant configuration
function plc.rps_init(reactor, is_formed, emer_cool)
local state_keys = { local state_keys = {
high_dmg = 1, high_dmg = 1,
high_temp = 2, high_temp = 2,
@ -54,6 +52,7 @@ function plc.rps_init(reactor, is_formed)
state = { false, false, false, false, false, false, false, false, false, false, false, false }, state = { false, false, false, false, false, false, false, false, false, false, false, false },
reactor_enabled = false, reactor_enabled = false,
enabled_at = 0, enabled_at = 0,
emer_cool_active = nil, ---@type boolean
formed = is_formed, formed = is_formed,
force_disabled = false, force_disabled = false,
tripped = false, tripped = false,
@ -74,6 +73,41 @@ function plc.rps_init(reactor, is_formed)
self.state[state_keys.fault] = false self.state[state_keys.fault] = false
end end
-- set emergency coolant control (if configured)
---@param state boolean true to enable emergency coolant, false to disable
local function _set_emer_cool(state)
-- check if this was configured: if it's a table, fields have already been validated.
if type(emer_cool) == "table" then
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state)
if level ~= false then
if rsio.is_color(emer_cool.color) then
local output = rs.getBundledOutput(emer_cool.side)
if rsio.digital_write(level) then
output = colors.combine(output, emer_cool.color)
else
output = colors.subtract(output, emer_cool.color)
end
rs.setBundledOutput(emer_cool.side, output)
else
rs.setOutput(emer_cool.side, rsio.digital_write(level))
end
if state ~= self.emer_cool_active then
if state then
log.info("RPS: emergency coolant valve OPENED")
else
log.info("RPS: emergency coolant valve CLOSED")
end
self.emer_cool_active = state
end
end
end
end
-- check if the reactor is formed -- check if the reactor is formed
local function _is_formed() local function _is_formed()
local formed = reactor.isFormed() local formed = reactor.isFormed()
@ -348,6 +382,12 @@ function plc.rps_init(reactor, is_formed)
end end
end end
-- update emergency coolant control if configured
_set_emer_cool(self.state[state_keys.low_coolant])
-- report RPS status
databus.tx_rps(self.tripped, self.state)
return self.tripped, status, first_trip return self.tripped, status, first_trip
end end
@ -358,6 +398,8 @@ function plc.rps_init(reactor, is_formed)
function public.is_tripped() return self.tripped end function public.is_tripped() return self.tripped end
---@nodiscard ---@nodiscard
function public.get_trip_cause() return self.trip_cause end function public.get_trip_cause() return self.trip_cause end
---@nodiscard
function public.is_low_coolant() return self.states[state_keys.low_coolant] end
---@nodiscard ---@nodiscard
function public.is_active() return self.reactor_enabled end function public.is_active() return self.reactor_enabled end
@ -397,6 +439,9 @@ function plc.rps_init(reactor, is_formed)
end end
end end
-- link functions with databus
databus.link_rps(public.trip_manual, public.reset)
return public return public
end end
@ -733,6 +778,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
---@param plc_state plc_state PLC state ---@param plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table ---@param setpoints setpoints setpoint control table
function public.handle_packet(packet, plc_state, setpoints) function public.handle_packet(packet, plc_state, setpoints)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not plc_state.fp_ok then util.println(message) end end
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
-- handle packets now that we have prints setup
if packet.scada_frame.local_port() == local_port then if packet.scada_frame.local_port() == local_port then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
@ -919,6 +969,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- clear this since this is for something that was unsolicited -- clear this since this is for something that was unsolicited
self.last_est_ack = ESTABLISH_ACK.ALLOW self.last_est_ack = ESTABLISH_ACK.ALLOW
-- report link state
databus.tx_link_state(est_ack + 1)
else else
log.debug("SCADA_MGMT establish packet length mismatch") log.debug("SCADA_MGMT establish packet length mismatch")
end end
@ -982,6 +1035,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
self.linked = est_ack == ESTABLISH_ACK.ALLOW self.linked = est_ack == ESTABLISH_ACK.ALLOW
self.last_est_ack = est_ack self.last_est_ack = est_ack
-- report link state
databus.tx_link_state(est_ack + 1)
else else
log.debug("SCADA_MGMT establish packet length mismatch") log.debug("SCADA_MGMT establish packet length mismatch")
end end

75
reactor-plc/renderer.lua Normal file
View File

@ -0,0 +1,75 @@
--
-- Graphics Rendering Control
--
local style = require("reactor-plc.panel.style")
local panel_view = require("reactor-plc.panel.front_panel")
local flasher = require("graphics.flasher")
local renderer = {}
local ui = {
view = nil
}
-- start the UI
function renderer.start_ui()
if ui.view == nil then
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
-- set overridden colors
for i = 1, #style.colors do
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
end
-- start flasher callback task
flasher.run()
-- init front panel view
ui.view = panel_view(term.current())
end
end
-- close out the UI
function renderer.close_ui()
-- stop blinking indicators
flasher.clear()
if ui.view ~= nil then
-- hide to stop animation callbacks
ui.view.hide()
end
-- clear root UI elements
ui.view = nil
-- restore colors
for i = 1, #style.colors do
local r, g, b = term.nativePaletteColor(style.colors[i].c)
term.setPaletteColor(style.colors[i].c, r, g, b)
end
-- reset terminal
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
end
-- is the UI ready?
---@nodiscard
---@return boolean ready
function renderer.ui_ready() return ui.view ~= nil end
-- handle a mouse event
---@param event mouse_interaction
function renderer.handle_mouse(event)
ui.view.handle_mouse(event)
end
return renderer

View File

@ -4,17 +4,21 @@
require("/initenv").init_env() require("/initenv").init_env()
local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("reactor-plc.config") local config = require("reactor-plc.config")
local databus = require("reactor-plc.databus")
local plc = require("reactor-plc.plc") local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.0.0" local R_PLC_VERSION = "v1.1.4"
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -39,6 +43,15 @@ cfv.assert_type_int(config.LOG_MODE)
assert(cfv.valid(), "bad config file: missing/invalid fields") assert(cfv.valid(), "bad config file: missing/invalid fields")
-- check emergency coolant configuration
if type(config.EMERGENCY_COOL) == "table" then
if not rsio.is_valid_side(config.EMERGENCY_COOL.side) then
assert(false, "bad config file: emergency coolant side unrecognized")
elseif config.EMERGENCY_COOL.color ~= nil and not rsio.is_color(config.EMERGENCY_COOL.color) then
assert(false, "bad config file: emergency coolant invalid redstone channel color provided")
end
end
---------------------------------------- ----------------------------------------
-- log init -- log init
---------------------------------------- ----------------------------------------
@ -61,6 +74,10 @@ local function main()
-- startup -- startup
---------------------------------------- ----------------------------------------
-- record firmware versions and ID
databus.tx_versions(R_PLC_VERSION, comms.version)
databus.tx_id(config.REACTOR_ID)
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
@ -74,6 +91,7 @@ local function main()
---@class plc_state ---@class plc_state
plc_state = { plc_state = {
init_ok = true, init_ok = true,
fp_ok = false,
shutdown = false, shutdown = false,
degraded = false, degraded = false,
reactor_formed = true, reactor_formed = true,
@ -145,17 +163,33 @@ local function main()
plc_state.no_modem = true plc_state.no_modem = true
end end
-- print a log message to the terminal as long as the UI isn't running
local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
-- PLC init<br> -- PLC init<br>
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
local function init() local function init()
if plc_state.init_ok then
-- just booting up, no fission allowed (neutrons stay put thanks) -- just booting up, no fission allowed (neutrons stay put thanks)
if plc_state.reactor_formed and smem_dev.reactor.getStatus() then if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram() smem_dev.reactor.scram()
end end
-- front panel time!
if not renderer.ui_ready() then
local message = nil
plc_state.fp_ok, message = pcall(renderer.start_ui)
if not plc_state.fp_ok then
renderer.close_ui()
println_ts(util.c("UI error: ", message))
println("init> running without front panel")
log.error(util.c("GUI crashed with error ", message))
log.info("init> running in headless mode without front panel")
end
end
if plc_state.init_ok then
-- init reactor protection system -- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed, config.EMERGENCY_COOL)
log.debug("init> rps init") log.debug("init> rps init")
if __shared_memory.networked then if __shared_memory.networked then
@ -168,18 +202,26 @@ local function main()
config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init") log.debug("init> comms init")
else else
println("init> starting in offline mode") _println_no_fp("init> starting in offline mode")
log.info("init> running without networking") log.info("init> running without networking")
end end
-- notify user of emergency coolant configuration status
if config.EMERGENCY_COOL ~= nil then
println("init> emergency coolant control ready")
log.info("init> running with emergency coolant control available")
end
util.push_event("clock_start") util.push_event("clock_start")
println("init> completed") _println_no_fp("init> completed")
log.info("init> startup completed") log.info("init> startup completed")
else else
println("init> system in degraded state, awaiting devices...") _println_no_fp("init> system in degraded state, awaiting devices...")
log.warning("init> started in a degraded state, awaiting peripheral connections...") log.warning("init> started in a degraded state, awaiting peripheral connections...")
end end
databus.tx_hw_status(plc_state)
end end
---------------------------------------- ----------------------------------------
@ -217,6 +259,8 @@ local function main()
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
end end
renderer.close_ui()
println_ts("exited") println_ts("exited")
log.info("exited") log.info("exited")
end end

View File

@ -1,14 +1,15 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcallbackdsp = require("scada-common.tcallbackdsp")
local util = require("scada-common.util") local util = require("scada-common.util")
local threads = {} local databus = require("reactor-plc.databus")
local renderer = require("reactor-plc.renderer")
local print = util.print local core = require("graphics.core")
local println = util.println
local print_ts = util.print_ts local threads = {}
local println_ts = util.println_ts
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RPS_SLEEP = 250 -- (250ms, 5 ticks) local RPS_SLEEP = 250 -- (250ms, 5 ticks)
@ -32,11 +33,16 @@ local MQ__COMM_CMD = {
---@param smem plc_shared_memory ---@param smem plc_shared_memory
---@param init function ---@param init function
function threads.thread__main(smem, init) function threads.thread__main(smem, init)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
---@class parallel_thread ---@class parallel_thread
local public = {} local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("main", true)
log.debug("main thread init, clock inactive") log.debug("main thread init, clock inactive")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- send status updates at 2Hz (every 10 server ticks) (every loop tick)
@ -61,6 +67,9 @@ function threads.thread__main(smem, init)
-- handle event -- handle event
if event == "timer" and loop_clock.is_clock(param1) then if event == "timer" and loop_clock.is_clock(param1) then
-- blink heartbeat indicator
databus.heartbeat()
-- core clock tick -- core clock tick
if networked then if networked then
-- start next clock timer -- start next clock timer
@ -133,6 +142,9 @@ function threads.thread__main(smem, init)
-- reactor no longer formed -- reactor no longer formed
plc_state.reactor_formed = false plc_state.reactor_formed = false
end end
-- update indicators
databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then
-- got a packet -- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
@ -144,6 +156,9 @@ function threads.thread__main(smem, init)
-- haven't heard from server recently? shutdown reactor -- haven't heard from server recently? shutdown reactor
plc_comms.unlink() plc_comms.unlink()
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
elseif event == "timer" then
-- notify timer callback dispatcher if no other timer case claimed this event
tcallbackdsp.handle(param1)
elseif event == "peripheral_detach" then elseif event == "peripheral_detach" then
-- peripheral disconnect -- peripheral disconnect
local type, device = ppm.handle_unmount(param1) local type, device = ppm.handle_unmount(param1)
@ -174,6 +189,9 @@ function threads.thread__main(smem, init)
end end
end end
end end
-- update indicators
databus.tx_hw_status(plc_state)
elseif event == "peripheral" then elseif event == "peripheral" then
-- peripheral connect -- peripheral connect
local type, device = ppm.mount(param1) local type, device = ppm.mount(param1)
@ -237,6 +255,12 @@ function threads.thread__main(smem, init)
plc_state.init_ok = true plc_state.init_ok = true
init() init()
end end
-- update indicators
databus.tx_hw_status(plc_state)
elseif event == "mouse_click" then
-- handle a monitor touch event
renderer.handle_mouse(core.events.click(param1, param2, param3))
elseif event == "clock_start" then elseif event == "clock_start" then
-- start loop clock -- start loop clock
loop_clock.start() loop_clock.start()
@ -263,6 +287,8 @@ function threads.thread__main(smem, init)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("main", false)
-- if status is true, then we are probably exiting, so this won't matter -- if status is true, then we are probably exiting, so this won't matter
-- if not, we need to restart the clock -- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
@ -280,11 +306,16 @@ end
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__rps(smem) function threads.thread__rps(smem)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
---@class parallel_thread ---@class parallel_thread
local public = {} local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("rps", true)
log.debug("rps thread start") log.debug("rps thread start")
-- load in from shared memory -- load in from shared memory
@ -314,16 +345,21 @@ function threads.thread__rps(smem)
rps.trip_timeout() rps.trip_timeout()
end end
else else
-- would do elseif not networked but there is no reason to do that extra operation
was_linked = true was_linked = true
end end
if (not plc_state.no_reactor) and rps.is_formed() then
-- check reactor status
---@diagnostic disable-next-line: need-check-nil
local reactor_status = reactor.getStatus()
databus.tx_reactor_state(reactor_status)
-- if we tried to SCRAM but failed, keep trying -- if we tried to SCRAM but failed, keep trying
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
---@diagnostic disable-next-line: need-check-nil if rps.is_tripped() and reactor_status then
if (not plc_state.no_reactor) and rps.is_formed() and rps.is_tripped() and reactor.getStatus() then
rps.scram() rps.scram()
end end
end
-- if we are in standalone mode, continuously reset RPS -- if we are in standalone mode, continuously reset RPS
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
@ -406,6 +442,8 @@ function threads.thread__rps(smem)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("rps", false)
if not plc_state.shutdown then if not plc_state.shutdown then
if plc_state.init_ok then smem.plc_sys.rps.scram() end if plc_state.init_ok then smem.plc_sys.rps.scram() end
log.info("rps thread restarting in 5 seconds...") log.info("rps thread restarting in 5 seconds...")
@ -426,6 +464,7 @@ function threads.thread__comms_tx(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_tx", true)
log.debug("comms tx thread start") log.debug("comms tx thread start")
-- load in from shared memory -- load in from shared memory
@ -483,6 +522,8 @@ function threads.thread__comms_tx(smem)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("comms_tx", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("comms tx thread restarting in 5 seconds...") log.info("comms tx thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
@ -502,6 +543,7 @@ function threads.thread__comms_rx(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_rx", true)
log.debug("comms rx thread start") log.debug("comms rx thread start")
-- load in from shared memory -- load in from shared memory
@ -559,6 +601,8 @@ function threads.thread__comms_rx(smem)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("comms_rx", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("comms rx thread restarting in 5 seconds...") log.info("comms rx thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
@ -578,6 +622,7 @@ function threads.thread__setpoint_control(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("spctl", true)
log.debug("setpoint control thread start") log.debug("setpoint control thread start")
-- load in from shared memory -- load in from shared memory
@ -692,6 +737,8 @@ function threads.thread__setpoint_control(smem)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("spctl", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("setpoint control thread restarting in 5 seconds...") log.info("setpoint control thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)

View File

@ -5,9 +5,13 @@ local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device -- create new boiler (mek 10.1+) device
---@nodiscard ---@nodiscard
---@param boiler table ---@param boiler table
---@return rtu_device interface, boolean faulted
function boilerv_rtu.new(boiler) function boilerv_rtu.new(boiler)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing
boiler.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(boiler.isFormed) unit.connect_di(boiler.isFormed)
@ -50,7 +54,12 @@ function boilerv_rtu.new(boiler)
-- holding registers -- -- holding registers --
-- none -- none
return unit.interface() -- check if any calls faulted
local faulted = boiler.__p_is_faulted()
boiler.__p_clear_fault()
boiler.__p_enable_afc()
return unit.interface(), faulted
end end
return boilerv_rtu return boilerv_rtu

View File

@ -5,9 +5,13 @@ local envd_rtu = {}
-- create new environment detector device -- create new environment detector device
---@nodiscard ---@nodiscard
---@param envd table ---@param envd table
---@return rtu_device interface, boolean faulted
function envd_rtu.new(envd) function envd_rtu.new(envd)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing
envd.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
-- none -- none
@ -21,7 +25,12 @@ function envd_rtu.new(envd)
-- holding registers -- -- holding registers --
-- none -- none
return unit.interface() -- check if any calls faulted
local faulted = envd.__p_is_faulted()
envd.__p_clear_fault()
envd.__p_enable_afc()
return unit.interface(), faulted
end end
return envd_rtu return envd_rtu

View File

@ -5,9 +5,13 @@ local imatrix_rtu = {}
-- create new induction matrix (mek 10.1+) device -- create new induction matrix (mek 10.1+) device
---@nodiscard ---@nodiscard
---@param imatrix table ---@param imatrix table
---@return rtu_device interface, boolean faulted
function imatrix_rtu.new(imatrix) function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing
imatrix.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(imatrix.isFormed) unit.connect_di(imatrix.isFormed)
@ -37,7 +41,12 @@ function imatrix_rtu.new(imatrix)
-- holding registers -- -- holding registers --
-- none -- none
return unit.interface() -- check if any calls faulted
local faulted = imatrix.__p_is_faulted()
imatrix.__p_clear_fault()
imatrix.__p_enable_afc()
return unit.interface(), faulted
end end
return imatrix_rtu return imatrix_rtu

View File

@ -11,6 +11,7 @@ local digital_write = rsio.digital_write
-- create new redstone device -- create new redstone device
---@nodiscard ---@nodiscard
---@return rtu_rs_device interface, boolean faulted
function redstone_rtu.new() function redstone_rtu.new()
local unit = rtu.init_unit() local unit = rtu.init_unit()
@ -111,7 +112,7 @@ function redstone_rtu.new()
) )
end end
return public return public, false
end end
return redstone_rtu return redstone_rtu

View File

@ -5,9 +5,13 @@ local sna_rtu = {}
-- create new solar neutron activator (SNA) device -- create new solar neutron activator (SNA) device
---@nodiscard ---@nodiscard
---@param sna table ---@param sna table
---@return rtu_device interface, boolean faulted
function sna_rtu.new(sna) function sna_rtu.new(sna)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing
sna.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
-- none -- none
@ -32,7 +36,12 @@ function sna_rtu.new(sna)
-- holding registers -- -- holding registers --
-- none -- none
return unit.interface() -- check if any calls faulted
local faulted = sna.__p_is_faulted()
sna.__p_clear_fault()
sna.__p_enable_afc()
return unit.interface(), faulted
end end
return sna_rtu return sna_rtu

View File

@ -5,9 +5,13 @@ local sps_rtu = {}
-- create new super-critical phase shifter (SPS) device -- create new super-critical phase shifter (SPS) device
---@nodiscard ---@nodiscard
---@param sps table ---@param sps table
---@return rtu_device interface, boolean faulted
function sps_rtu.new(sps) function sps_rtu.new(sps)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing
sps.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(sps.isFormed) unit.connect_di(sps.isFormed)
@ -42,7 +46,12 @@ function sps_rtu.new(sps)
-- holding registers -- -- holding registers --
-- none -- none
return unit.interface() -- check if any calls faulted
local faulted = sps.__p_is_faulted()
sps.__p_clear_fault()
sps.__p_enable_afc()
return unit.interface(), faulted
end end
return sps_rtu return sps_rtu

View File

@ -5,9 +5,13 @@ local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device -- create new turbine (mek 10.1+) device
---@nodiscard ---@nodiscard
---@param turbine table ---@param turbine table
---@return rtu_device interface, boolean faulted
function turbinev_rtu.new(turbine) function turbinev_rtu.new(turbine)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- disable auto fault clearing
turbine.__p_disable_afc()
-- discrete inputs -- -- discrete inputs --
unit.connect_di(turbine.isFormed) unit.connect_di(turbine.isFormed)
@ -49,7 +53,12 @@ function turbinev_rtu.new(turbine)
-- holding registers -- -- holding registers --
unit.connect_holding_reg(turbine.getDumpingMode, turbine.setDumpingMode) unit.connect_holding_reg(turbine.getDumpingMode, turbine.setDumpingMode)
return unit.interface() -- check if any calls faulted
local faulted = turbine.__p_is_faulted()
turbine.__p_clear_fault()
turbine.__p_enable_afc()
return unit.interface(), faulted
end end
return turbinev_rtu return turbinev_rtu

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 = "v0.13.0" local RTU_VERSION = "v0.13.2"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@ -290,8 +290,9 @@ local function main()
local type = nil ---@type string|nil local type = nil ---@type string|nil
local rtu_iface = nil ---@type rtu_device local rtu_iface = nil ---@type rtu_device
local rtu_type = nil ---@type RTU_UNIT_TYPE local rtu_type = nil ---@type RTU_UNIT_TYPE
local is_multiblock = false local is_multiblock = false ---@type boolean
local formed = nil ---@type boolean|nil local formed = nil ---@type boolean|nil
local faulted = nil ---@type boolean|nil
if device == nil then if device == nil then
local message = util.c("configure> '", name, "' not found, using placeholder") local message = util.c("configure> '", name, "' not found, using placeholder")
@ -307,7 +308,7 @@ local function main()
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface = boilerv_rtu.new(device) rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -319,7 +320,7 @@ local function main()
elseif type == "turbineValve" then elseif type == "turbineValve" then
-- turbine multiblock -- turbine multiblock
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface = turbinev_rtu.new(device) rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -331,7 +332,7 @@ local function main()
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface = imatrix_rtu.new(device) rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -343,7 +344,7 @@ local function main()
elseif type == "spsPort" then elseif type == "spsPort" then
-- SPS multiblock -- SPS multiblock
rtu_type = RTU_UNIT_TYPE.SPS rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface = sps_rtu.new(device) rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -355,11 +356,11 @@ local function main()
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
-- SNA -- SNA
rtu_type = RTU_UNIT_TYPE.SNA rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface = sna_rtu.new(device) rtu_iface, _ = sna_rtu.new(device)
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface = envd_rtu.new(device) rtu_iface, _ = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device -- placeholder device
rtu_type = RTU_UNIT_TYPE.VIRTUAL rtu_type = RTU_UNIT_TYPE.VIRTUAL
@ -371,6 +372,17 @@ local function main()
return false return false
end end
if is_multiblock then
if not formed then
log.info(util.c("configure> device '", name, "' is not formed"))
elseif faulted then
-- sometimes there is a race condition on server boot where it reports formed, but
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
formed = false
log.warning(util.c("configure> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
end
end
---@class rtu_unit_registry_entry ---@class rtu_unit_registry_entry
local rtu_unit = { local rtu_unit = {
uid = 0, ---@type integer uid = 0, ---@type integer
@ -391,10 +403,6 @@ local function main()
table.insert(units, rtu_unit) table.insert(units, rtu_unit)
if is_multiblock and not formed then
log.info(util.c("configure> device '", name, "' is not formed"))
end
local for_message = "facility" local for_message = "facility"
if for_reactor > 0 then if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor) for_message = util.c("reactor ", for_reactor)

View File

@ -375,37 +375,43 @@ function threads.thread__unit_comms(smem, unit)
ppm.unmount(unit.device) ppm.unmount(unit.device)
local type, device = ppm.mount(iface) local type, device = ppm.mount(iface)
local faulted = false
if device ~= nil then if device ~= nil then
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler multiblock -- boiler multiblock
unit.device = device unit.device = device
unit.rtu = boilerv_rtu.new(device) unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
-- turbine multiblock -- turbine multiblock
unit.device = device unit.device = device
unit.rtu = turbinev_rtu.new(device) unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock -- induction matrix multiblock
unit.device = device unit.device = device
unit.rtu = imatrix_rtu.new(device) unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
-- SPS multiblock -- SPS multiblock
unit.device = device unit.device = device
unit.rtu = sps_rtu.new(device) unit.rtu, faulted = sps_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
else else
log.error("illegal remount of non-multiblock RTU attempted for " .. short_name, true) log.error("illegal remount of non-multiblock RTU attempted for " .. short_name, true)
end end
if unit.formed and faulted then
-- something is still wrong = can't mark as formed yet
unit.formed = false
else
rtu_comms.send_remounted(unit.uid) rtu_comms.send_remounted(unit.uid)
end
else else
-- fully lost the peripheral now :( -- fully lost the peripheral now :(
log.error(util.c(unit.name, " lost (failed reconnect)")) log.error(util.c(unit.name, " lost (failed reconnect)"))

View File

@ -51,6 +51,19 @@ function psil.create()
self.ic[key].value = value self.ic[key].value = value
end end
-- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed<br>
-- this is intended to be used to toggle boolean indicators such as heartbeats without extra state variables
---@param key string data key
function public.toggle(key)
if self.ic[key] == nil then alloc(key) end
self.ic[key].value = self.ic[key].value == false
for i = 1, #self.ic[key].subscribers do
self.ic[key].subscribers[i].notify(self.ic[key].value)
end
end
return public return public
end 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 = "v0.14.0" local SUPERVISOR_VERSION = "v0.14.3"
local print = util.print local print = util.print
local println = util.println local println = util.println

View File

@ -154,7 +154,8 @@ function unit.new(reactor_id, num_boilers, num_turbines)
ReactorHighWaste = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighWaste, tier = PRIO.URGENT }, ReactorHighWaste = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.ReactorHighWaste, tier = PRIO.URGENT },
-- RPS trip occured -- RPS trip occured
RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.RPSTransient, tier = PRIO.TIMELY }, RPSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.RPSTransient, tier = PRIO.TIMELY },
-- BoilRateMismatch, CoolantFeedMismatch, SteamFeedMismatch, MaxWaterReturnFeed -- CoolantLevelLow, WaterLevelLow, TurbineOverSpeed, MaxWaterReturnFeed, RCPTrip, RCSFlowLow, BoilRateMismatch, CoolantFeedMismatch,
-- SteamFeedMismatch, MaxWaterReturnFeed, RCS hardware fault
RCSTransient = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 5, id = ALARM.RCSTransient, tier = PRIO.TIMELY }, 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, "The China Syndrome" -- "It's just a routine turbin' trip!" -Bill Gibson, "The China Syndrome"
TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.TurbineTrip, tier = PRIO.URGENT } TurbineTrip = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 2, id = ALARM.TurbineTrip, tier = PRIO.URGENT }
@ -195,6 +196,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
TurbineOnline = {}, TurbineOnline = {},
SteamDumpOpen = {}, SteamDumpOpen = {},
TurbineOverSpeed = {}, TurbineOverSpeed = {},
GeneratorTrip = {},
TurbineTrip = {} TurbineTrip = {}
}, },
---@class alarms ---@class alarms
@ -238,6 +240,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
table.insert(self.db.annunciator.TurbineOnline, false) table.insert(self.db.annunciator.TurbineOnline, false)
table.insert(self.db.annunciator.SteamDumpOpen, TRI_FAIL.OK) table.insert(self.db.annunciator.SteamDumpOpen, TRI_FAIL.OK)
table.insert(self.db.annunciator.TurbineOverSpeed, false) table.insert(self.db.annunciator.TurbineOverSpeed, false)
table.insert(self.db.annunciator.GeneratorTrip, false)
table.insert(self.db.annunciator.TurbineTrip, false) table.insert(self.db.annunciator.TurbineTrip, false)
end end
@ -312,7 +315,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
local last_update_s = db.tanks.last_update / 1000.0 local last_update_s = db.tanks.last_update / 1000.0
_compute_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx(), db.tanks.steam.amount, last_update_s) _compute_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx(), db.tanks.steam.amount, last_update_s)
---@todo unused currently?
_compute_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx(), db.tanks.energy, last_update_s) _compute_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx(), db.tanks.energy, last_update_s)
end end
end end

View File

@ -126,19 +126,10 @@ function logic.update_annunciator(self)
self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
-- this warning applies when no coolant is buffered (which we can't easily determine without running)
--[[
logic is that each tick, the heating rate worth of coolant steps between:
reactor tank
reactor heated coolant outflow tube
boiler/turbine tank
reactor cooled coolant return tube
so if there is a tick where coolant is no longer present in the reactor, then bad things happen.
such as when a burn rate consumes half the coolant in the tank, meaning that:
50% at some point will be in the boiler, and 50% in a tube, so that leaves 0% in the reactor
]]--
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000) local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
local high_rate = (plc_db.mek_status.ccool_amnt / (plc_db.mek_status.burn_rate * heating_rate_conv)) < 4 local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv)
-- this advisory applies when no coolant is buffered (which we can't easily determine)<br>
-- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate
-- 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
@ -316,12 +307,13 @@ function logic.update_annunciator(self)
self.db.annunciator.SteamFeedMismatch = sfmismatch self.db.annunciator.SteamFeedMismatch = sfmismatch
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0 self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
-- check if steam dumps are open -- turbine safety checks
for i = 1, #self.turbines do for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session local turbine = self.turbines[i] ---@type unit_session
local db = turbine.get_db() ---@type turbinev_session_db local db = turbine.get_db() ---@type turbinev_session_db
local idx = turbine.get_device_idx() local idx = turbine.get_device_idx()
-- check if steam dumps are open
if db.state.dumping_mode == DUMPING_MODE.IDLE then if db.state.dumping_mode == DUMPING_MODE.IDLE then
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK
elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then
@ -329,16 +321,19 @@ function logic.update_annunciator(self)
else else
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL
end end
end
-- check if turbines are at max speed but not keeping up -- check if turbines are at max speed but not keeping up
for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session
local db = turbine.get_db() ---@type turbinev_session_db
local idx = turbine.get_device_idx()
self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0) self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0)
end
--[[
Generator Trip
a generator trip is when a generator suddenly and unexpectedly loses it's external load
oftentimes this is when a power plant is disconnected from the grid for one reason or another
in this case we just:
- check if internal power storage of turbine is increasing
that means there is no external load and there will be a turbine trip soon if this is not resolved
]]--
self.db.annunciator.GeneratorTrip[idx] = _get_dt(DT_KEYS.TurbinePower .. idx) > 0.0
--[[ --[[
Turbine Trip Turbine Trip
@ -348,12 +343,8 @@ function logic.update_annunciator(self)
- can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up - can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up
- can later identified by presence of steam in tank with a 0 flow rate - can later identified by presence of steam in tank with a 0 flow rate
]]-- ]]--
for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session
local db = turbine.get_db() ---@type turbinev_session_db
local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01 local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01
self.db.annunciator.TurbineTrip[turbine.get_device_idx()] = has_steam and db.state.flow_rate == 0 self.db.annunciator.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0
end end
-- update auto control ready state for this unit -- update auto control ready state for this unit
@ -506,10 +497,12 @@ function logic.update_alarms(self)
-- RCS Transient -- RCS Transient
local any_low = annunc.CoolantLevelLow local any_low = annunc.CoolantLevelLow
local any_over = false local any_over = false
local gen_trip = false
for i = 1, #annunc.WaterLevelLow do any_low = any_low or annunc.WaterLevelLow[i] end 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 for i = 1, #annunc.TurbineOverSpeed do any_over = any_over or annunc.TurbineOverSpeed[i] end
for i = 1, #annunc.GeneratorTrip do gen_trip = gen_trip or annunc.GeneratorTrip[i] end
local rcs_trans = any_low or any_over or annunc.RCPTrip or annunc.MaxWaterReturnFeed local rcs_trans = any_low or any_over or gen_trip or annunc.RCPTrip or annunc.MaxWaterReturnFeed
-- only care about RCS flow low early with boilers -- only care about RCS flow low early with boilers
if self.num_boilers > 0 then rcs_trans = rcs_trans or annunc.RCSFlowLow end if self.num_boilers > 0 then rcs_trans = rcs_trans or annunc.RCSFlowLow end