diff --git a/ccmsi.lua b/ccmsi.lua
index f728d01..962d5f0 100644
--- a/ccmsi.lua
+++ b/ccmsi.lua
@@ -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
-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,
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.
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
diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua
index fc54fb3..cfb3be7 100644
--- a/coordinator/iocontrol.lua
+++ b/coordinator/iocontrol.lua
@@ -683,23 +683,13 @@ function iocontrol.update_unit_statuses(statuses)
end
for key, val in pairs(unit.annunciator) do
- if key == "TurbineTrip" 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
+ if key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
for id = 1, #val do
unit.boiler_ps_tbl[id].publish(key, val[id])
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
for id = 1, #val do
unit.turbine_ps_tbl[id].publish(key, val[id])
diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua
index edf0de8..f0563ca 100644
--- a/coordinator/renderer.lua
+++ b/coordinator/renderer.lua
@@ -171,15 +171,15 @@ end
function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event
----@param event monitor_touch
-function renderer.handle_touch(event)
+---@param event mouse_interaction
+function renderer.handle_mouse(event)
if event.monitor == engine.monitors.primary_name then
- ui.main_layout.handle_touch(event)
+ ui.main_layout.handle_mouse(event)
else
for id, monitor in pairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then
local layout = ui.unit_layouts[id] ---@type graphics_element
- layout.handle_touch(event)
+ layout.handle_mouse(event)
end
end
end
diff --git a/coordinator/startup.lua b/coordinator/startup.lua
index 39629fd..b169b06 100644
--- a/coordinator/startup.lua
+++ b/coordinator/startup.lua
@@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
-local COORDINATOR_VERSION = "v0.12.2"
+local COORDINATOR_VERSION = "v0.12.5"
local print = util.print
local println = util.println
@@ -354,7 +354,7 @@ local function main()
end
elseif event == "monitor_touch" then
-- 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
-- handle speaker buffer emptied
sounder.continue()
diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua
index b6fc8af..a29dd4b 100644
--- a/coordinator/ui/components/unit_detail.lua
+++ b/coordinator/ui/components/unit_detail.lua
@@ -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}
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_tags = Div{parent=rcs,width=2,height=14,x=29,y=9}
+ local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
+ 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_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_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_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("EmergencyCoolant", c_emg.update)
@@ -252,12 +251,19 @@ local function init(parent, id)
u_ps.subscribe("BoilRateMismatch", c_brm.update)
u_ps.subscribe("SteamFeedMismatch", c_sfm.update)
u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
- u_ps.subscribe("TurbineTrip", c_tbnt.update)
- rcs_annunc.line_break()
+ 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()
+ end
-- boiler annunciator panel(s)
+ if available_space > 0 then _add_space() end
+
if unit.num_boilers > 0 then
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)}
@@ -268,6 +274,13 @@ local function init(parent, id)
b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
end
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}
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)
@@ -279,14 +292,9 @@ local function init(parent, id)
-- turbine annunciator panels
- if unit.num_boilers == 0 then
- 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
+ if available_space > 1 then _add_space() 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}
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)}
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}
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)
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}
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)
@@ -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)}
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}
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)
end
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}
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)
@@ -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)}
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}
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)
diff --git a/graphics/core.lua b/graphics/core.lua
index 98c8ed5..d03e551 100644
--- a/graphics/core.lua
+++ b/graphics/core.lua
@@ -10,20 +10,76 @@ core.flasher = flasher
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 button integer
---@field x integer
---@field y integer
--- create a new touch event definition
+-- create a new monitor touch mouse interaction event
---@nodiscard
---@param monitor string
---@param x integer
---@param y integer
----@return monitor_touch
+---@return mouse_interaction
function events.touch(monitor, x, y)
return {
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,
y = y
}
diff --git a/graphics/element.lua b/graphics/element.lua
index 8aa3ce9..d9bc489 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -32,6 +32,9 @@ local element = {}
---|data_indicator_args
---|hbar_args
---|icon_indicator_args
+---|indicator_led_args
+---|indicator_led_pair_args
+---|indicator_led_rgb_args
---|indicator_light_args
---|power_indicator_args
---|rad_indicator_args
@@ -100,7 +103,13 @@ function element.new(args)
else
local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1
- protected.frame.y = args.y or next_y
+
+ if args.parent ~= nil then
+ protected.frame.y = args.y or (next_y - offset_y)
+ else
+ protected.frame.y = args.y or next_y
+ end
+
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
@@ -157,9 +166,9 @@ function element.new(args)
self.bounds.y2 = self.position.y + f.h - 1
end
- -- handle a touch event
- ---@param event table monitor_touch event
- function protected.handle_touch(event)
+ -- handle a mouse event
+ ---@param event mouse_interaction mouse interaction event
+ function protected.handle_mouse(event)
end
-- handle data value changes
@@ -260,6 +269,11 @@ function element.new(args)
---@param child graphics_template
---@return integer|string key
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)
self.next_y = child.frame.y + child.frame.h
@@ -396,20 +410,20 @@ function element.new(args)
-- FUNCTION CALLBACKS --
- -- handle a monitor touch
- ---@param event monitor_touch monitor touch event
- function public.handle_touch(event)
+ -- handle a monitor touch or mouse click
+ ---@param event mouse_interaction mouse interaction event
+ function public.handle_mouse(event)
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
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
- protected.handle_touch(event_T)
+ protected.handle_mouse(event_T)
-- 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
diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua
index 0b59df6..e9f2bf4 100644
--- a/graphics/elements/controls/hazard_button.lua
+++ b/graphics/elements/controls/hazard_button.lua
@@ -140,10 +140,10 @@ local function hazard_button(args)
end
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
- function e.handle_touch(event)
+ function e.handle_mouse(event)
if e.enabled then
-- change text color to indicate clicked
e.window.setTextColor(args.accent)
@@ -178,7 +178,7 @@ local function hazard_button(args)
-- set the value (true simulates pressing the button)
---@param val boolean new value
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
-- show the button as disabled
diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua
index 2cf583a..2549e2b 100644
--- a/graphics/elements/controls/multi_button.lua
+++ b/graphics/elements/controls/multi_button.lua
@@ -92,9 +92,10 @@ local function multi_button(args)
end
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
- function e.handle_touch(event)
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+---@diagnostic disable-next-line: unused-local
+ function e.handle_mouse(event)
-- determine what was pressed
if e.enabled and event.y == 1 then
for i = 1, #args.options do
diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua
index 8cb89c9..d0c1299 100644
--- a/graphics/elements/controls/push_button.lua
+++ b/graphics/elements/controls/push_button.lua
@@ -8,7 +8,7 @@ local element = require("graphics.element")
---@class push_button_args
---@field text string button text
---@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 dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
@@ -47,10 +47,10 @@ local function push_button(args)
e.window.write(args.text)
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
- function e.handle_touch(event)
+ function e.handle_mouse(event)
if e.enabled then
if args.active_fg_bg ~= nil then
-- show as pressed
@@ -78,7 +78,7 @@ local function push_button(args)
-- set the value (true simulates pressing the button)
---@param val boolean new value
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
-- show butten as enabled
diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua
index 025cad1..3b2a593 100644
--- a/graphics/elements/controls/radio_button.lua
+++ b/graphics/elements/controls/radio_button.lua
@@ -79,9 +79,9 @@ local function radio_button(args)
end
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
- function e.handle_touch(event)
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
if args.options[event.y] ~= nil then
diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua
index 088d847..ffbd1f8 100644
--- a/graphics/elements/controls/spinbox_numeric.lua
+++ b/graphics/elements/controls/spinbox_numeric.lua
@@ -127,9 +127,9 @@ local function spinbox(args)
-- init with the default value
show_num()
- -- handle touch
- ---@param event monitor_touch monitor touch event
- function e.handle_touch(event)
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled and event.x ~= dec_point_x then
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua
index bf138f2..133ea45 100644
--- a/graphics/elements/controls/switch_button.lua
+++ b/graphics/elements/controls/switch_button.lua
@@ -62,10 +62,10 @@ local function switch_button(args)
-- initial draw
draw_state()
- -- handle touch
- ---@param event monitor_touch monitor touch event
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
- function e.handle_touch(event)
+ function e.handle_mouse(event)
if e.enabled then
-- toggle state
e.value = not e.value
diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua
new file mode 100644
index 0000000..7905848
--- /dev/null
+++ b/graphics/elements/indicators/led.lua
@@ -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
diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua
new file mode 100644
index 0000000..aaf94ec
--- /dev/null
+++ b/graphics/elements/indicators/ledpair.lua
@@ -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
diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua
new file mode 100644
index 0000000..c58b835
--- /dev/null
+++ b/graphics/elements/indicators/ledrgb.lua
@@ -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
diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua
index 6422cbc..2f7a68d 100644
--- a/graphics/elements/rectangle.lua
+++ b/graphics/elements/rectangle.lua
@@ -7,6 +7,7 @@ local element = require("graphics.element")
---@class rectangle_args
---@field border? graphics_border
---@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 id? string element id
---@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)
-- partial bars
- local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
- local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
- local p_s = spaces
-
+ local p_a, p_b, p_s
if args.thin == true then
- p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
- p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
+ 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_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
+ end
+
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
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
e.window.blit(p_a, p_inv_bg, p_inv_fg)
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
- e.window.blit(p_b, p_inv_bg, p_inv_fg)
+ e.window.blit(p_b, _fg, _bg)
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
-- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides)
@@ -129,12 +146,19 @@ local function rectangle(args)
-- partial pixel fill
if args.border.even and y == ((e.frame.h - border_width) + 1) 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
+ 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
- e.window.blit(p_a, p_inv_fg, blit_bg_top_bot)
- elseif width_x2 % 3 == 2 or (args.thin == true) then
- e.window.blit(p_b, p_inv_fg, blit_bg_top_bot)
+ e.window.blit(p_a, _fg, _bg)
+ elseif width_x2 % 3 == 2 then
+ e.window.blit(p_b, _fg, _bg)
else
-- skip line
e.window.blit(spaces, blit_fg, blit_bg_sides)
diff --git a/imgen.py b/imgen.py
index faab46a..30e7e69 100644
--- a/imgen.py
+++ b/imgen.py
@@ -7,7 +7,7 @@ def list_files(path):
for (root, dirs, files) in os.walk(path):
for f in files:
- list.append(root[2:] + "/" + f)
+ list.append((root[2:] + "/" + f).replace('\\','/'))
return list
@@ -68,7 +68,7 @@ def make_manifest(size):
"pocket" : list_files("./pocket"),
},
"depends" : {
- "reactor-plc" : [ "system", "common" ],
+ "reactor-plc" : [ "system", "common", "graphics" ],
"rtu" : [ "system", "common" ],
"supervisor" : [ "system", "common" ],
"coordinator" : [ "system", "common", "graphics" ],
diff --git a/install_manifest.json b/install_manifest.json
index 78bb226..d898b5b 100644
--- a/install_manifest.json
+++ b/install_manifest.json
@@ -1 +1 @@
-{"versions": {"installer": "v1.0", "bootloader": "0.2", "comms": "1.4.0", "reactor-plc": "v1.0.0", "rtu": "v0.13.0", "supervisor": "v0.14.0", "coordinator": "v0.12.2", "pocket": "alpha-v0.0.0"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/crypto.lua", "scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/tcallbackdsp.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua"], "graphics": ["graphics/element.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/cipher/aes128.lua", "lockbox/cipher/aes256.lua", "lockbox/cipher/aes192.lua", "lockbox/cipher/mode/ofb.lua", "lockbox/cipher/mode/cbc.lua", "lockbox/cipher/mode/ctr.lua", "lockbox/cipher/mode/cfb.lua", "lockbox/mac/hmac.lua", "lockbox/padding/ansix923.lua", "lockbox/padding/pkcs7.lua", "lockbox/padding/zero.lua", "lockbox/padding/isoiec7816.lua"], "reactor-plc": ["reactor-plc/threads.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua"], "rtu": ["rtu/threads.lua", "rtu/rtu.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/apisessions.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/processctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/unit_waiting.lua", "coordinator/ui/components/turbine.lua"], "pocket": ["pocket/config.lua", "pocket/startup.lua"]}, "depends": {"reactor-plc": ["system", "common"], "rtu": ["system", "common"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 4646, "system": 1982, "common": 91084, "graphics": 99858, "lockbox": 100797, "reactor-plc": 75529, "rtu": 82913, "supervisor": 274491, "coordinator": 180346, "pocket": 335}}
\ No newline at end of file
+{"versions": {"installer": "v1.0", "bootloader": "0.2", "comms": "1.4.0", "reactor-plc": "v1.1.4", "rtu": "v0.13.2", "supervisor": "v0.14.3", "coordinator": "v0.12.5", "pocket": "alpha-v0.0.0"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/crypto.lua", "scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/tcallbackdsp.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua"], "graphics": ["graphics/element.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/cipher/aes128.lua", "lockbox/cipher/aes256.lua", "lockbox/cipher/aes192.lua", "lockbox/cipher/mode/ofb.lua", "lockbox/cipher/mode/cbc.lua", "lockbox/cipher/mode/ctr.lua", "lockbox/cipher/mode/cfb.lua", "lockbox/mac/hmac.lua", "lockbox/padding/ansix923.lua", "lockbox/padding/pkcs7.lua", "lockbox/padding/zero.lua", "lockbox/padding/isoiec7816.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/threads.lua", "rtu/rtu.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/apisessions.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/processctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/unit_waiting.lua", "coordinator/ui/components/turbine.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/config.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 5040, "system": 1982, "common": 91616, "graphics": 111150, "lockbox": 100797, "reactor-plc": 95084, "rtu": 86291, "supervisor": 274510, "coordinator": 181151, "pocket": 6873}}
\ No newline at end of file
diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua
index f3cf0f6..0555b54 100644
--- a/reactor-plc/config.lua
+++ b/reactor-plc/config.lua
@@ -5,6 +5,10 @@ config.NETWORKED = true
-- unique reactor ID
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
config.SERVER_PORT = 16000
-- port to listen to incoming packets FROM server
diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua
new file mode 100644
index 0000000..eeb2260
--- /dev/null
+++ b/reactor-plc/databus.lua
@@ -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
diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua
new file mode 100644
index 0000000..f875b5d
--- /dev/null
+++ b/reactor-plc/panel/front_panel.lua
@@ -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
diff --git a/reactor-plc/panel/style.lua b/reactor-plc/panel/style.lua
new file mode 100644
index 0000000..01b00c9
--- /dev/null
+++ b/reactor-plc/panel/style.lua
@@ -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
diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua
index dce6a73..2879290 100644
--- a/reactor-plc/plc.lua
+++ b/reactor-plc/plc.lua
@@ -1,9 +1,11 @@
-local comms = require("scada-common.comms")
-local const = require("scada-common.constants")
-local log = require("scada-common.log")
-local ppm = require("scada-common.ppm")
-local types = require("scada-common.types")
-local util = require("scada-common.util")
+local comms = require("scada-common.comms")
+local const = require("scada-common.constants")
+local databus = require("reactor-plc.databus")
+local log = require("scada-common.log")
+local ppm = require("scada-common.ppm")
+local rsio = require("scada-common.rsio")
+local types = require("scada-common.types")
+local util = require("scada-common.util")
local plc = {}
@@ -18,11 +20,6 @@ local AUTO_ACK = comms.PLC_AUTO_ACK
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 wish they didn't change it to be like this
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
---@param reactor table
---@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 = {
high_dmg = 1,
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 },
reactor_enabled = false,
enabled_at = 0,
+ emer_cool_active = nil, ---@type boolean
formed = is_formed,
force_disabled = false,
tripped = false,
@@ -74,6 +73,41 @@ function plc.rps_init(reactor, is_formed)
self.state[state_keys.fault] = false
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
local function _is_formed()
local formed = reactor.isFormed()
@@ -348,6 +382,12 @@ function plc.rps_init(reactor, is_formed)
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
end
@@ -358,6 +398,8 @@ function plc.rps_init(reactor, is_formed)
function public.is_tripped() return self.tripped end
---@nodiscard
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
function public.is_active() return self.reactor_enabled end
@@ -397,6 +439,9 @@ function plc.rps_init(reactor, is_formed)
end
end
+ -- link functions with databus
+ databus.link_rps(public.trip_manual, public.reset)
+
return public
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 setpoints setpoints setpoint control table
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
-- check sequence number
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
self.last_est_ack = ESTABLISH_ACK.ALLOW
+
+ -- report link state
+ databus.tx_link_state(est_ack + 1)
else
log.debug("SCADA_MGMT establish packet length mismatch")
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.last_est_ack = est_ack
+
+ -- report link state
+ databus.tx_link_state(est_ack + 1)
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
diff --git a/reactor-plc/renderer.lua b/reactor-plc/renderer.lua
new file mode 100644
index 0000000..ad2bcc0
--- /dev/null
+++ b/reactor-plc/renderer.lua
@@ -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
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index 5ad5ca4..293281c 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -4,17 +4,21 @@
require("/initenv").init_env()
-local crash = require("scada-common.crash")
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local ppm = require("scada-common.ppm")
-local util = require("scada-common.util")
+local comms = require("scada-common.comms")
+local crash = require("scada-common.crash")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local ppm = require("scada-common.ppm")
+local rsio = require("scada-common.rsio")
+local util = require("scada-common.util")
-local config = require("reactor-plc.config")
-local plc = require("reactor-plc.plc")
-local threads = require("reactor-plc.threads")
+local config = require("reactor-plc.config")
+local databus = require("reactor-plc.databus")
+local plc = require("reactor-plc.plc")
+local renderer = require("reactor-plc.renderer")
+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 println = util.println
@@ -39,6 +43,15 @@ cfv.assert_type_int(config.LOG_MODE)
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
----------------------------------------
@@ -61,6 +74,10 @@ local function main()
-- startup
----------------------------------------
+ -- record firmware versions and ID
+ databus.tx_versions(R_PLC_VERSION, comms.version)
+ databus.tx_id(config.REACTOR_ID)
+
-- mount connected devices
ppm.mount_all()
@@ -74,6 +91,7 @@ local function main()
---@class plc_state
plc_state = {
init_ok = true,
+ fp_ok = false,
shutdown = false,
degraded = false,
reactor_formed = true,
@@ -145,17 +163,33 @@ local function main()
plc_state.no_modem = true
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
--- EVENT_CONSUMER: this function consumes events
local function init()
- if plc_state.init_ok then
- -- just booting up, no fission allowed (neutrons stay put thanks)
- if plc_state.reactor_formed and smem_dev.reactor.getStatus() then
- smem_dev.reactor.scram()
- end
+ -- just booting up, no fission allowed (neutrons stay put thanks)
+ if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
+ smem_dev.reactor.scram()
+ 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
- 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")
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)
log.debug("init> comms init")
else
- println("init> starting in offline mode")
+ _println_no_fp("init> starting in offline mode")
log.info("init> running without networking")
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")
- println("init> completed")
+ _println_no_fp("init> completed")
log.info("init> startup completed")
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...")
end
+
+ databus.tx_hw_status(plc_state)
end
----------------------------------------
@@ -217,6 +259,8 @@ local function main()
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
end
+ renderer.close_ui()
+
println_ts("exited")
log.info("exited")
end
diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua
index d2708fd..c307999 100644
--- a/reactor-plc/threads.lua
+++ b/reactor-plc/threads.lua
@@ -1,15 +1,16 @@
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local ppm = require("scada-common.ppm")
-local util = require("scada-common.util")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local ppm = require("scada-common.ppm")
+local tcallbackdsp = require("scada-common.tcallbackdsp")
+local util = require("scada-common.util")
+
+local databus = require("reactor-plc.databus")
+local renderer = require("reactor-plc.renderer")
+
+local core = require("graphics.core")
local threads = {}
-local print = util.print
-local println = util.println
-local print_ts = util.print_ts
-local println_ts = util.println_ts
-
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
@@ -32,11 +33,16 @@ local MQ__COMM_CMD = {
---@param smem plc_shared_memory
---@param init function
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
local public = {}
-- execute thread
function public.exec()
+ databus.tx_rt_status("main", true)
log.debug("main thread init, clock inactive")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
@@ -61,6 +67,9 @@ function threads.thread__main(smem, init)
-- handle event
if event == "timer" and loop_clock.is_clock(param1) then
+ -- blink heartbeat indicator
+ databus.heartbeat()
+
-- core clock tick
if networked then
-- start next clock timer
@@ -133,6 +142,9 @@ function threads.thread__main(smem, init)
-- reactor no longer formed
plc_state.reactor_formed = false
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
-- got a packet
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
plc_comms.unlink()
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
-- peripheral disconnect
local type, device = ppm.handle_unmount(param1)
@@ -174,6 +189,9 @@ function threads.thread__main(smem, init)
end
end
end
+
+ -- update indicators
+ databus.tx_hw_status(plc_state)
elseif event == "peripheral" then
-- peripheral connect
local type, device = ppm.mount(param1)
@@ -237,6 +255,12 @@ function threads.thread__main(smem, init)
plc_state.init_ok = true
init()
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
-- start loop clock
loop_clock.start()
@@ -263,6 +287,8 @@ function threads.thread__main(smem, init)
log.fatal(util.strval(result))
end
+ databus.tx_rt_status("main", false)
+
-- if status is true, then we are probably exiting, so this won't matter
-- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
@@ -280,11 +306,16 @@ end
---@nodiscard
---@param smem plc_shared_memory
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
local public = {}
-- execute thread
function public.exec()
+ databus.tx_rt_status("rps", true)
log.debug("rps thread start")
-- load in from shared memory
@@ -314,15 +345,20 @@ function threads.thread__rps(smem)
rps.trip_timeout()
end
else
- -- would do elseif not networked but there is no reason to do that extra operation
was_linked = true
end
- -- 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)
+ if (not plc_state.no_reactor) and rps.is_formed() then
+ -- check reactor status
---@diagnostic disable-next-line: need-check-nil
- if (not plc_state.no_reactor) and rps.is_formed() and rps.is_tripped() and reactor.getStatus() then
- rps.scram()
+ local reactor_status = reactor.getStatus()
+ databus.tx_reactor_state(reactor_status)
+
+ -- 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)
+ if rps.is_tripped() and reactor_status then
+ rps.scram()
+ end
end
-- if we are in standalone mode, continuously reset RPS
@@ -406,6 +442,8 @@ function threads.thread__rps(smem)
log.fatal(util.strval(result))
end
+ databus.tx_rt_status("rps", false)
+
if not plc_state.shutdown then
if plc_state.init_ok then smem.plc_sys.rps.scram() end
log.info("rps thread restarting in 5 seconds...")
@@ -426,6 +464,7 @@ function threads.thread__comms_tx(smem)
-- execute thread
function public.exec()
+ databus.tx_rt_status("comms_tx", true)
log.debug("comms tx thread start")
-- load in from shared memory
@@ -483,6 +522,8 @@ function threads.thread__comms_tx(smem)
log.fatal(util.strval(result))
end
+ databus.tx_rt_status("comms_tx", false)
+
if not plc_state.shutdown then
log.info("comms tx thread restarting in 5 seconds...")
util.psleep(5)
@@ -502,6 +543,7 @@ function threads.thread__comms_rx(smem)
-- execute thread
function public.exec()
+ databus.tx_rt_status("comms_rx", true)
log.debug("comms rx thread start")
-- load in from shared memory
@@ -559,6 +601,8 @@ function threads.thread__comms_rx(smem)
log.fatal(util.strval(result))
end
+ databus.tx_rt_status("comms_rx", false)
+
if not plc_state.shutdown then
log.info("comms rx thread restarting in 5 seconds...")
util.psleep(5)
@@ -578,6 +622,7 @@ function threads.thread__setpoint_control(smem)
-- execute thread
function public.exec()
+ databus.tx_rt_status("spctl", true)
log.debug("setpoint control thread start")
-- load in from shared memory
@@ -692,6 +737,8 @@ function threads.thread__setpoint_control(smem)
log.fatal(util.strval(result))
end
+ databus.tx_rt_status("spctl", false)
+
if not plc_state.shutdown then
log.info("setpoint control thread restarting in 5 seconds...")
util.psleep(5)
diff --git a/rtu/dev/boilerv_rtu.lua b/rtu/dev/boilerv_rtu.lua
index b93d412..46ac7c2 100644
--- a/rtu/dev/boilerv_rtu.lua
+++ b/rtu/dev/boilerv_rtu.lua
@@ -5,9 +5,13 @@ local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device
---@nodiscard
---@param boiler table
+---@return rtu_device interface, boolean faulted
function boilerv_rtu.new(boiler)
local unit = rtu.init_unit()
+ -- disable auto fault clearing
+ boiler.__p_disable_afc()
+
-- discrete inputs --
unit.connect_di(boiler.isFormed)
@@ -50,7 +54,12 @@ function boilerv_rtu.new(boiler)
-- holding registers --
-- 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
return boilerv_rtu
diff --git a/rtu/dev/envd_rtu.lua b/rtu/dev/envd_rtu.lua
index ba4758a..2894e2c 100644
--- a/rtu/dev/envd_rtu.lua
+++ b/rtu/dev/envd_rtu.lua
@@ -5,9 +5,13 @@ local envd_rtu = {}
-- create new environment detector device
---@nodiscard
---@param envd table
+---@return rtu_device interface, boolean faulted
function envd_rtu.new(envd)
local unit = rtu.init_unit()
+ -- disable auto fault clearing
+ envd.__p_disable_afc()
+
-- discrete inputs --
-- none
@@ -21,7 +25,12 @@ function envd_rtu.new(envd)
-- holding registers --
-- 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
return envd_rtu
diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua
index 29405b8..3b72a12 100644
--- a/rtu/dev/imatrix_rtu.lua
+++ b/rtu/dev/imatrix_rtu.lua
@@ -5,9 +5,13 @@ local imatrix_rtu = {}
-- create new induction matrix (mek 10.1+) device
---@nodiscard
---@param imatrix table
+---@return rtu_device interface, boolean faulted
function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit()
+ -- disable auto fault clearing
+ imatrix.__p_disable_afc()
+
-- discrete inputs --
unit.connect_di(imatrix.isFormed)
@@ -37,7 +41,12 @@ function imatrix_rtu.new(imatrix)
-- holding registers --
-- 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
return imatrix_rtu
diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua
index da7db6b..c482999 100644
--- a/rtu/dev/redstone_rtu.lua
+++ b/rtu/dev/redstone_rtu.lua
@@ -11,6 +11,7 @@ local digital_write = rsio.digital_write
-- create new redstone device
---@nodiscard
+---@return rtu_rs_device interface, boolean faulted
function redstone_rtu.new()
local unit = rtu.init_unit()
@@ -111,7 +112,7 @@ function redstone_rtu.new()
)
end
- return public
+ return public, false
end
return redstone_rtu
diff --git a/rtu/dev/sna_rtu.lua b/rtu/dev/sna_rtu.lua
index 0339794..16c0cfd 100644
--- a/rtu/dev/sna_rtu.lua
+++ b/rtu/dev/sna_rtu.lua
@@ -5,9 +5,13 @@ local sna_rtu = {}
-- create new solar neutron activator (SNA) device
---@nodiscard
---@param sna table
+---@return rtu_device interface, boolean faulted
function sna_rtu.new(sna)
local unit = rtu.init_unit()
+ -- disable auto fault clearing
+ sna.__p_disable_afc()
+
-- discrete inputs --
-- none
@@ -32,7 +36,12 @@ function sna_rtu.new(sna)
-- holding registers --
-- 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
return sna_rtu
diff --git a/rtu/dev/sps_rtu.lua b/rtu/dev/sps_rtu.lua
index ba0a18c..349550c 100644
--- a/rtu/dev/sps_rtu.lua
+++ b/rtu/dev/sps_rtu.lua
@@ -5,9 +5,13 @@ local sps_rtu = {}
-- create new super-critical phase shifter (SPS) device
---@nodiscard
---@param sps table
+---@return rtu_device interface, boolean faulted
function sps_rtu.new(sps)
local unit = rtu.init_unit()
+ -- disable auto fault clearing
+ sps.__p_disable_afc()
+
-- discrete inputs --
unit.connect_di(sps.isFormed)
@@ -42,7 +46,12 @@ function sps_rtu.new(sps)
-- holding registers --
-- 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
return sps_rtu
diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua
index 89b3ae0..4f2ee48 100644
--- a/rtu/dev/turbinev_rtu.lua
+++ b/rtu/dev/turbinev_rtu.lua
@@ -5,9 +5,13 @@ local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device
---@nodiscard
---@param turbine table
+---@return rtu_device interface, boolean faulted
function turbinev_rtu.new(turbine)
local unit = rtu.init_unit()
+ -- disable auto fault clearing
+ turbine.__p_disable_afc()
+
-- discrete inputs --
unit.connect_di(turbine.isFormed)
@@ -49,7 +53,12 @@ function turbinev_rtu.new(turbine)
-- holding registers --
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
return turbinev_rtu
diff --git a/rtu/startup.lua b/rtu/startup.lua
index df79496..40c0498 100644
--- a/rtu/startup.lua
+++ b/rtu/startup.lua
@@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
-local RTU_VERSION = "v0.13.0"
+local RTU_VERSION = "v0.13.2"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@@ -290,8 +290,9 @@ local function main()
local type = nil ---@type string|nil
local rtu_iface = nil ---@type rtu_device
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 faulted = nil ---@type boolean|nil
if device == nil then
local message = util.c("configure> '", name, "' not found, using placeholder")
@@ -307,7 +308,7 @@ local function main()
if type == "boilerValve" then
-- boiler multiblock
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
- rtu_iface = boilerv_rtu.new(device)
+ rtu_iface, faulted = boilerv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -319,7 +320,7 @@ local function main()
elseif type == "turbineValve" then
-- turbine multiblock
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
- rtu_iface = turbinev_rtu.new(device)
+ rtu_iface, faulted = turbinev_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -331,7 +332,7 @@ local function main()
elseif type == "inductionPort" then
-- induction matrix multiblock
rtu_type = RTU_UNIT_TYPE.IMATRIX
- rtu_iface = imatrix_rtu.new(device)
+ rtu_iface, faulted = imatrix_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -343,7 +344,7 @@ local function main()
elseif type == "spsPort" then
-- SPS multiblock
rtu_type = RTU_UNIT_TYPE.SPS
- rtu_iface = sps_rtu.new(device)
+ rtu_iface, faulted = sps_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -355,11 +356,11 @@ local function main()
elseif type == "solarNeutronActivator" then
-- SNA
rtu_type = RTU_UNIT_TYPE.SNA
- rtu_iface = sna_rtu.new(device)
+ rtu_iface, _ = sna_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment 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
-- placeholder device
rtu_type = RTU_UNIT_TYPE.VIRTUAL
@@ -371,6 +372,17 @@ local function main()
return false
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
local rtu_unit = {
uid = 0, ---@type integer
@@ -391,10 +403,6 @@ local function main()
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"
if for_reactor > 0 then
for_message = util.c("reactor ", for_reactor)
diff --git a/rtu/threads.lua b/rtu/threads.lua
index 6b06eb0..27ad68e 100644
--- a/rtu/threads.lua
+++ b/rtu/threads.lua
@@ -375,37 +375,43 @@ function threads.thread__unit_comms(smem, unit)
ppm.unmount(unit.device)
local type, device = ppm.mount(iface)
+ local faulted = false
if device ~= nil then
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler multiblock
unit.device = device
- unit.rtu = boilerv_rtu.new(device)
+ unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
-- turbine multiblock
unit.device = device
- unit.rtu = turbinev_rtu.new(device)
+ unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock
unit.device = device
- unit.rtu = imatrix_rtu.new(device)
+ unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
-- SPS multiblock
unit.device = device
- unit.rtu = sps_rtu.new(device)
+ unit.rtu, faulted = sps_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
else
log.error("illegal remount of non-multiblock RTU attempted for " .. short_name, true)
end
- rtu_comms.send_remounted(unit.uid)
+ 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)
+ end
else
-- fully lost the peripheral now :(
log.error(util.c(unit.name, " lost (failed reconnect)"))
diff --git a/scada-common/psil.lua b/scada-common/psil.lua
index c21b2cf..664d10d 100644
--- a/scada-common/psil.lua
+++ b/scada-common/psil.lua
@@ -51,6 +51,19 @@ function psil.create()
self.ic[key].value = value
end
+ -- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed
+ -- 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
end
diff --git a/supervisor/startup.lua b/supervisor/startup.lua
index 2d0c577..14e7bb3 100644
--- a/supervisor/startup.lua
+++ b/supervisor/startup.lua
@@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions")
local config = require("supervisor.config")
local supervisor = require("supervisor.supervisor")
-local SUPERVISOR_VERSION = "v0.14.0"
+local SUPERVISOR_VERSION = "v0.14.3"
local print = util.print
local println = util.println
diff --git a/supervisor/unit.lua b/supervisor/unit.lua
index abdcbb0..0cccc48 100644
--- a/supervisor/unit.lua
+++ b/supervisor/unit.lua
@@ -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 },
-- RPS trip occured
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 },
-- "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 }
@@ -195,6 +196,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
TurbineOnline = {},
SteamDumpOpen = {},
TurbineOverSpeed = {},
+ GeneratorTrip = {},
TurbineTrip = {}
},
---@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.SteamDumpOpen, TRI_FAIL.OK)
table.insert(self.db.annunciator.TurbineOverSpeed, false)
+ table.insert(self.db.annunciator.GeneratorTrip, false)
table.insert(self.db.annunciator.TurbineTrip, false)
end
@@ -312,7 +315,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
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)
- ---@todo unused currently?
_compute_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx(), db.tanks.energy, last_update_s)
end
end
diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua
index 4aa41d2..ca97181 100644
--- a/supervisor/unitlogic.lua
+++ b/supervisor/unitlogic.lua
@@ -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.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 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)
+ -- 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
-- 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.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
local turbine = self.turbines[i] ---@type unit_session
local db = turbine.get_db() ---@type turbinev_session_db
local idx = turbine.get_device_idx()
+ -- check if steam dumps are open
if db.state.dumping_mode == DUMPING_MODE.IDLE then
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK
elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then
@@ -329,31 +321,30 @@ function logic.update_annunciator(self)
else
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL
end
- end
-
- -- 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()
+ -- check if turbines are at max speed but not keeping up
self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0)
- end
- --[[
- Turbine Trip
- a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool.
- this can be identified by these conditions:
- - the current flow rate is 0 mB/t and it should not be
- - 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
- ]]--
- for i = 1, #self.turbines do
- local turbine = self.turbines[i] ---@type unit_session
- local db = turbine.get_db() ---@type turbinev_session_db
+ --[[
+ 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
+ a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool.
+ this can be identified by these conditions:
+ - the current flow rate is 0 mB/t and it should not be
+ - 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
+ ]]--
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
-- update auto control ready state for this unit
@@ -506,10 +497,12 @@ function logic.update_alarms(self)
-- RCS Transient
local any_low = annunc.CoolantLevelLow
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.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
if self.num_boilers > 0 then rcs_trans = rcs_trans or annunc.RCSFlowLow end