mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
commit
04d73cdcd3
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
98
graphics/elements/indicators/led.lua
Normal file
98
graphics/elements/indicators/led.lua
Normal file
@ -0,0 +1,98 @@
|
||||
-- Indicator "LED" Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_led_args
|
||||
---@field label string indicator label
|
||||
---@field colors cpair on/off colors (a/b respectively)
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash on true rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new indicator LED
|
||||
---@nodiscard
|
||||
---@param args indicator_led_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- enable light or start flashing
|
||||
local function enable()
|
||||
if args.flash then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- disable light or stop flashing
|
||||
local function disable()
|
||||
if args.flash then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state boolean indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
if new_state then enable() else disable() end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return indicator_led
|
112
graphics/elements/indicators/ledpair.lua
Normal file
112
graphics/elements/indicators/ledpair.lua
Normal file
@ -0,0 +1,112 @@
|
||||
-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b)
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
---@class indicator_led_pair_args
|
||||
---@field label string indicator label
|
||||
---@field off color color for off
|
||||
---@field c1 color color for #1 on
|
||||
---@field c2 color color for #2 on
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field flash? boolean whether to flash when on rather than stay on
|
||||
---@field period? PERIOD flash period
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new dual LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_pair_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_pair(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
|
||||
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local co = colors.toBlit(args.off)
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return indicator_led_pair
|
57
graphics/elements/indicators/ledrgb.lua
Normal file
57
graphics/elements/indicators/ledrgb.lua
Normal file
@ -0,0 +1,57 @@
|
||||
-- Indicator RGB LED Graphics Element
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class indicator_led_rgb_args
|
||||
---@field label string indicator label
|
||||
---@field colors table colors to use
|
||||
---@field min_label_width? integer label length if omitted
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new RGB LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_rgb_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_rgb(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
if type(args.colors[new_state]) == "number" then
|
||||
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
return e.get()
|
||||
end
|
||||
|
||||
return indicator_led_rgb
|
@ -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)
|
||||
|
4
imgen.py
4
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" ],
|
||||
|
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
101
reactor-plc/databus.lua
Normal file
101
reactor-plc/databus.lua
Normal file
@ -0,0 +1,101 @@
|
||||
--
|
||||
-- Data Bus - Central Communication Linking for PLC Front Panel
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local psil = require("scada-common.psil")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = {}
|
||||
|
||||
local dbus_iface = {
|
||||
ps = psil.create(),
|
||||
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
|
||||
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
|
||||
}
|
||||
|
||||
-- call to toggle heartbeat signal
|
||||
function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
|
||||
|
||||
-- link RPS command functions
|
||||
---@param scram function reactor SCRAM function
|
||||
---@param reset function RPS reset function
|
||||
function databus.link_rps(scram, reset)
|
||||
dbus_iface.rps_scram = scram
|
||||
dbus_iface.rps_reset = reset
|
||||
end
|
||||
|
||||
-- transmit a command to the RPS to SCRAM
|
||||
function databus.rps_scram() dbus_iface.rps_scram() end
|
||||
|
||||
-- transmit a command to the RPS to reset
|
||||
function databus.rps_reset() dbus_iface.rps_reset() end
|
||||
|
||||
-- transmit firmware versions across the bus
|
||||
---@param plc_v string PLC version
|
||||
---@param comms_v string comms version
|
||||
function databus.tx_versions(plc_v, comms_v)
|
||||
dbus_iface.ps.publish("version", plc_v)
|
||||
dbus_iface.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- transmit unit ID across the bus
|
||||
---@param id integer unit ID
|
||||
function databus.tx_id(id)
|
||||
dbus_iface.ps.publish("unit_id", id)
|
||||
end
|
||||
|
||||
-- transmit hardware status across the bus
|
||||
---@param plc_state plc_state
|
||||
function databus.tx_hw_status(plc_state)
|
||||
dbus_iface.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
|
||||
dbus_iface.ps.publish("has_modem", not plc_state.no_modem)
|
||||
dbus_iface.ps.publish("degraded", plc_state.degraded)
|
||||
dbus_iface.ps.publish("init_ok", plc_state.init_ok)
|
||||
end
|
||||
|
||||
-- transmit thread (routine) statuses
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function databus.tx_rt_status(thread, ok)
|
||||
dbus_iface.ps.publish(util.c("routine__", thread), ok)
|
||||
end
|
||||
|
||||
-- transmit supervisor link state across the bus
|
||||
---@param state integer
|
||||
function databus.tx_link_state(state)
|
||||
dbus_iface.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
-- transmit reactor enable state across the bus
|
||||
---@param active boolean reactor active
|
||||
function databus.tx_reactor_state(active)
|
||||
dbus_iface.ps.publish("reactor_active", active)
|
||||
end
|
||||
|
||||
-- transmit RPS data across the bus
|
||||
---@param tripped boolean RPS tripped
|
||||
---@param status table RPS status
|
||||
function databus.tx_rps(tripped, status)
|
||||
dbus_iface.ps.publish("rps_scram", tripped)
|
||||
dbus_iface.ps.publish("rps_damage", status[1])
|
||||
dbus_iface.ps.publish("rps_high_temp", status[2])
|
||||
dbus_iface.ps.publish("rps_low_ccool", status[3])
|
||||
dbus_iface.ps.publish("rps_high_waste", status[4])
|
||||
dbus_iface.ps.publish("rps_high_hcool", status[5])
|
||||
dbus_iface.ps.publish("rps_no_fuel", status[6])
|
||||
dbus_iface.ps.publish("rps_fault", status[7])
|
||||
dbus_iface.ps.publish("rps_timeout", status[8])
|
||||
dbus_iface.ps.publish("rps_manual", status[9])
|
||||
dbus_iface.ps.publish("rps_automatic", status[10])
|
||||
dbus_iface.ps.publish("rps_sysfail", status[11])
|
||||
end
|
||||
|
||||
-- link a function to receive data from the bus
|
||||
---@param field string field name
|
||||
---@param func function function to link
|
||||
function databus.rx_field(field, func)
|
||||
dbus_iface.ps.subscribe(field, func)
|
||||
end
|
||||
|
||||
return databus
|
124
reactor-plc/panel/front_panel.lua
Normal file
124
reactor-plc/panel/front_panel.lua
Normal file
@ -0,0 +1,124 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("reactor-plc.databus")
|
||||
|
||||
local style = require("reactor-plc.panel.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local LEDPair = require("graphics.elements.indicators.ledpair")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
local border = core.graphics.border
|
||||
|
||||
-- create new main view
|
||||
---@param monitor table main viewscreen
|
||||
local function init(monitor)
|
||||
local panel = DisplayBox{window=monitor,fg_bg=style.root}
|
||||
|
||||
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
databus.rx_field("unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
|
||||
|
||||
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
|
||||
|
||||
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
|
||||
system.line_break()
|
||||
|
||||
databus.rx_field("init_ok", init_ok.update)
|
||||
databus.rx_field("heartbeat", heartbeat.update)
|
||||
|
||||
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
|
||||
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(5)
|
||||
system.line_break()
|
||||
|
||||
databus.rx_field("reactor_dev_state", reactor.update)
|
||||
databus.rx_field("has_modem", modem.update)
|
||||
databus.rx_field("link_state", network.update)
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
|
||||
local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
|
||||
local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)}
|
||||
local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.green_off)}
|
||||
local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)}
|
||||
system.line_break()
|
||||
|
||||
databus.rx_field("routine__main", rt_main.update)
|
||||
databus.rx_field("routine__rps", rt_rps.update)
|
||||
databus.rx_field("routine__comms_tx", rt_cmtx.update)
|
||||
databus.rx_field("routine__comms_rx", rt_cmrx.update)
|
||||
databus.rx_field("routine__spctl", rt_sctl.update)
|
||||
|
||||
local status = Div{parent=panel,width=19,height=18,x=17,y=3}
|
||||
|
||||
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)}
|
||||
|
||||
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,y=2,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
||||
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS}
|
||||
|
||||
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,y=5,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
|
||||
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
|
||||
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
|
||||
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
|
||||
|
||||
databus.rx_field("reactor_active", active.update)
|
||||
databus.rx_field("rps_scram", scram.update)
|
||||
|
||||
local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
|
||||
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
|
||||
databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
|
||||
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)}
|
||||
rps.line_break()
|
||||
local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)}
|
||||
rps.line_break()
|
||||
local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)}
|
||||
rps.line_break()
|
||||
local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)}
|
||||
local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)}
|
||||
|
||||
databus.rx_field("rps_manual", rps_man.update)
|
||||
databus.rx_field("rps_automatic", rps_auto.update)
|
||||
databus.rx_field("rps_timeout", rps_tmo.update)
|
||||
databus.rx_field("rps_fault", rps_flt.update)
|
||||
databus.rx_field("rps_sysfail", rps_fail.update)
|
||||
databus.rx_field("rps_damage", rps_dmg.update)
|
||||
databus.rx_field("rps_high_temp", rps_tmp.update)
|
||||
databus.rx_field("rps_no_fuel", rps_nof.update)
|
||||
databus.rx_field("rps_high_waste", rps_wst.update)
|
||||
databus.rx_field("rps_low_ccool", rps_ccl.update)
|
||||
databus.rx_field("rps_high_hcool", rps_hcl.update)
|
||||
|
||||
return panel
|
||||
end
|
||||
|
||||
return init
|
41
reactor-plc/panel/style.lua
Normal file
41
reactor-plc/panel/style.lua
Normal file
@ -0,0 +1,41 @@
|
||||
--
|
||||
-- Graphics Style Options
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local style = {}
|
||||
|
||||
local cpair = core.graphics.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
style.root = cpair(colors.black, colors.ivory)
|
||||
style.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 },
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
return style
|
@ -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
|
||||
|
75
reactor-plc/renderer.lua
Normal file
75
reactor-plc/renderer.lua
Normal file
@ -0,0 +1,75 @@
|
||||
--
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local style = require("reactor-plc.panel.style")
|
||||
local panel_view = require("reactor-plc.panel.front_panel")
|
||||
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local renderer = {}
|
||||
|
||||
local ui = {
|
||||
view = nil
|
||||
}
|
||||
|
||||
-- start the UI
|
||||
function renderer.start_ui()
|
||||
if ui.view == nil then
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- init front panel view
|
||||
ui.view = panel_view(term.current())
|
||||
end
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
|
||||
if ui.view ~= nil then
|
||||
-- hide to stop animation callbacks
|
||||
ui.view.hide()
|
||||
end
|
||||
|
||||
-- clear root UI elements
|
||||
ui.view = nil
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
|
||||
-- is the UI ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return ui.view ~= nil end
|
||||
|
||||
-- handle a mouse event
|
||||
---@param event mouse_interaction
|
||||
function renderer.handle_mouse(event)
|
||||
ui.view.handle_mouse(event)
|
||||
end
|
||||
|
||||
return renderer
|
@ -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<br>
|
||||
--- 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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)"))
|
||||
|
@ -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<br>
|
||||
-- this is intended to be used to toggle boolean indicators such as heartbeats without extra state variables
|
||||
---@param key string data key
|
||||
function public.toggle(key)
|
||||
if self.ic[key] == nil then alloc(key) end
|
||||
|
||||
self.ic[key].value = self.ic[key].value == false
|
||||
|
||||
for i = 1, #self.ic[key].subscribers do
|
||||
self.ic[key].subscribers[i].notify(self.ic[key].value)
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)<br>
|
||||
-- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation
|
||||
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate
|
||||
|
||||
-- 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
|
||||
|
Loading…
Reference in New Issue
Block a user