mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
Merge pull request #196 from MikaylaFischler/front-panels
PLC Front Panel
This commit is contained in:
commit
c9b67f68dd
@ -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.3"
|
||||
local COORDINATOR_VERSION = "v0.12.4"
|
||||
|
||||
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()
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
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("\x83", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. util.strrep("\x8f", 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)
|
||||
|
2
imgen.py
2
imgen.py
@ -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" ],
|
||||
|
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,5 +1,6 @@
|
||||
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")
|
||||
@ -19,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."
|
||||
@ -389,6 +385,9 @@ function plc.rps_init(reactor, is_formed, emer_cool)
|
||||
-- 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
|
||||
|
||||
@ -440,6 +439,9 @@ function plc.rps_init(reactor, is_formed, emer_cool)
|
||||
end
|
||||
end
|
||||
|
||||
-- link functions with databus
|
||||
databus.link_rps(public.trip_manual, public.reset)
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
@ -776,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
|
||||
@ -962,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
|
||||
@ -1025,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,6 +4,7 @@
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
@ -12,10 +13,12 @@ local rsio = require("scada-common.rsio")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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.1"
|
||||
local R_PLC_VERSION = "v1.1.4"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -71,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()
|
||||
|
||||
@ -84,6 +91,7 @@ local function main()
|
||||
---@class plc_state
|
||||
plc_state = {
|
||||
init_ok = true,
|
||||
fp_ok = false,
|
||||
shutdown = false,
|
||||
degraded = false,
|
||||
reactor_formed = true,
|
||||
@ -155,15 +163,31 @@ 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
|
||||
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, config.EMERGENCY_COOL)
|
||||
log.debug("init> rps init")
|
||||
@ -178,7 +202,7 @@ 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
|
||||
|
||||
@ -190,12 +214,14 @@ local function main()
|
||||
|
||||
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
|
||||
|
||||
----------------------------------------
|
||||
@ -233,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,14 +1,15 @@
|
||||
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 threads = {}
|
||||
local databus = require("reactor-plc.databus")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
local core = require("graphics.core")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RPS_SLEEP = 250 -- (250ms, 5 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
|
||||
@ -317,12 +348,18 @@ function threads.thread__rps(smem)
|
||||
was_linked = true
|
||||
end
|
||||
|
||||
if (not plc_state.no_reactor) and rps.is_formed() then
|
||||
-- check reactor status
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local reactor_status = reactor.getStatus()
|
||||
databus.tx_reactor_state(reactor_status)
|
||||
|
||||
-- if we tried to SCRAM but failed, keep trying
|
||||
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
if (not plc_state.no_reactor) and rps.is_formed() and rps.is_tripped() and reactor.getStatus() then
|
||||
if rps.is_tripped() and reactor_status then
|
||||
rps.scram()
|
||||
end
|
||||
end
|
||||
|
||||
-- if we are in standalone mode, continuously reset RPS
|
||||
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
|
||||
@ -405,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...")
|
||||
@ -425,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
|
||||
@ -482,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)
|
||||
@ -501,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
|
||||
@ -558,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)
|
||||
@ -577,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
|
||||
@ -691,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)
|
||||
|
@ -16,7 +16,7 @@ local MODE = {
|
||||
log.MODE = MODE
|
||||
|
||||
-- whether to log debug messages or not
|
||||
local LOG_DEBUG = false
|
||||
local LOG_DEBUG = true
|
||||
|
||||
local log_sys = {
|
||||
path = "/log.txt",
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user