diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua
index edf0de8..f0563ca 100644
--- a/coordinator/renderer.lua
+++ b/coordinator/renderer.lua
@@ -171,15 +171,15 @@ end
function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event
----@param event monitor_touch
-function renderer.handle_touch(event)
+---@param event mouse_interaction
+function renderer.handle_mouse(event)
if event.monitor == engine.monitors.primary_name then
- ui.main_layout.handle_touch(event)
+ ui.main_layout.handle_mouse(event)
else
for id, monitor in pairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then
local layout = ui.unit_layouts[id] ---@type graphics_element
- layout.handle_touch(event)
+ layout.handle_mouse(event)
end
end
end
diff --git a/coordinator/startup.lua b/coordinator/startup.lua
index a6a81f1..95178ad 100644
--- a/coordinator/startup.lua
+++ b/coordinator/startup.lua
@@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
-local COORDINATOR_VERSION = "v0.12.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()
diff --git a/graphics/core.lua b/graphics/core.lua
index 98c8ed5..d03e551 100644
--- a/graphics/core.lua
+++ b/graphics/core.lua
@@ -10,20 +10,76 @@ core.flasher = flasher
local events = {}
----@class monitor_touch
+---@enum click_type
+events.click_type = {
+ VIRTUAL = 0,
+ LEFT_BUTTON = 1,
+ RIGHT_BUTTON = 2,
+ MID_BUTTON = 3
+}
+
+---@class mouse_interaction
---@field monitor string
+---@field button integer
---@field x integer
---@field y integer
--- create a new touch event definition
+-- create a new monitor touch mouse interaction event
---@nodiscard
---@param monitor string
---@param x integer
---@param y integer
----@return monitor_touch
+---@return mouse_interaction
function events.touch(monitor, x, y)
return {
monitor = monitor,
+ button = events.click_type.LEFT_BUTTON,
+ x = x,
+ y = y
+ }
+end
+
+-- create a new mouse click mouse interaction event
+---@nodiscard
+---@param button click_type
+---@param x integer
+---@param y integer
+---@return mouse_interaction
+function events.click(button, x, y)
+ return {
+ monitor = "terminal",
+ button = button,
+ x = x,
+ y = y
+ }
+end
+
+-- create a new transposed mouse interaction event using the event's monitor/button fields
+---@nodiscard
+---@param event mouse_interaction
+---@param new_x integer
+---@param new_y integer
+---@return mouse_interaction
+function events.mouse_transposed(event, new_x, new_y)
+ return {
+ monitor = event.monitor,
+ button = event.button,
+ x = new_x,
+ y = new_y
+ }
+end
+
+-- create a new generic mouse interaction event
+---@nodiscard
+---@param monitor string
+---@param button click_type
+---@param x integer
+---@param y integer
+---@return mouse_interaction
+function events.mouse_generic(monitor, button, x, y)
+ return {
+ monitor = monitor,
+ button = button,
x = x,
y = y
}
diff --git a/graphics/element.lua b/graphics/element.lua
index 8aa3ce9..d9bc489 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -32,6 +32,9 @@ local element = {}
---|data_indicator_args
---|hbar_args
---|icon_indicator_args
+---|indicator_led_args
+---|indicator_led_pair_args
+---|indicator_led_rgb_args
---|indicator_light_args
---|power_indicator_args
---|rad_indicator_args
@@ -100,7 +103,13 @@ function element.new(args)
else
local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1
- protected.frame.y = args.y or next_y
+
+ if args.parent ~= nil then
+ protected.frame.y = args.y or (next_y - offset_y)
+ else
+ protected.frame.y = args.y or next_y
+ end
+
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
@@ -157,9 +166,9 @@ function element.new(args)
self.bounds.y2 = self.position.y + f.h - 1
end
- -- handle a touch event
- ---@param event table monitor_touch event
- function protected.handle_touch(event)
+ -- handle a mouse event
+ ---@param event mouse_interaction mouse interaction event
+ function protected.handle_mouse(event)
end
-- handle data value changes
@@ -260,6 +269,11 @@ function element.new(args)
---@param child graphics_template
---@return integer|string key
function public.__add_child(key, child)
+ -- offset first automatic placement
+ if self.next_y <= self.child_offset.y then
+ self.next_y = self.child_offset.y + 1
+ end
+
child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
self.next_y = child.frame.y + child.frame.h
@@ -396,20 +410,20 @@ function element.new(args)
-- FUNCTION CALLBACKS --
- -- handle a monitor touch
- ---@param event monitor_touch monitor touch event
- function public.handle_touch(event)
+ -- handle a monitor touch or mouse click
+ ---@param event mouse_interaction mouse interaction event
+ function public.handle_mouse(event)
local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
if in_x and in_y then
- local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
+ local event_T = core.events.mouse_transposed(event, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
-- handle the touch event, transformed into the window frame
- protected.handle_touch(event_T)
+ protected.handle_mouse(event_T)
-- pass on touch event to children
- for _, val in pairs(self.children) do val.handle_touch(event_T) end
+ for _, val in pairs(self.children) do val.handle_mouse(event_T) end
end
end
diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua
index 0b59df6..e9f2bf4 100644
--- a/graphics/elements/controls/hazard_button.lua
+++ b/graphics/elements/controls/hazard_button.lua
@@ -140,10 +140,10 @@ local function hazard_button(args)
end
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
- function e.handle_touch(event)
+ function e.handle_mouse(event)
if e.enabled then
-- change text color to indicate clicked
e.window.setTextColor(args.accent)
@@ -178,7 +178,7 @@ local function hazard_button(args)
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_touch(core.events.touch("", 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
end
-- show the button as disabled
diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua
index 2cf583a..2549e2b 100644
--- a/graphics/elements/controls/multi_button.lua
+++ b/graphics/elements/controls/multi_button.lua
@@ -92,9 +92,10 @@ local function multi_button(args)
end
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
- function e.handle_touch(event)
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+---@diagnostic disable-next-line: unused-local
+ function e.handle_mouse(event)
-- determine what was pressed
if e.enabled and event.y == 1 then
for i = 1, #args.options do
diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua
index 8cb89c9..d0c1299 100644
--- a/graphics/elements/controls/push_button.lua
+++ b/graphics/elements/controls/push_button.lua
@@ -8,7 +8,7 @@ local element = require("graphics.element")
---@class push_button_args
---@field text string button text
---@field callback function function to call on touch
----@field min_width? integer text length + 2 if omitted
+---@field min_width? integer text length if omitted
---@field active_fg_bg? cpair foreground/background colors when pressed
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
@@ -47,10 +47,10 @@ local function push_button(args)
e.window.write(args.text)
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
- function e.handle_touch(event)
+ function e.handle_mouse(event)
if e.enabled then
if args.active_fg_bg ~= nil then
-- show as pressed
@@ -78,7 +78,7 @@ local function push_button(args)
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_touch(core.events.touch("", 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
end
-- show butten as enabled
diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua
index 025cad1..3b2a593 100644
--- a/graphics/elements/controls/radio_button.lua
+++ b/graphics/elements/controls/radio_button.lua
@@ -79,9 +79,9 @@ local function radio_button(args)
end
end
- -- handle touch
- ---@param event monitor_touch monitor touch event
- function e.handle_touch(event)
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
if args.options[event.y] ~= nil then
diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua
index 088d847..ffbd1f8 100644
--- a/graphics/elements/controls/spinbox_numeric.lua
+++ b/graphics/elements/controls/spinbox_numeric.lua
@@ -127,9 +127,9 @@ local function spinbox(args)
-- init with the default value
show_num()
- -- handle touch
- ---@param event monitor_touch monitor touch event
- function e.handle_touch(event)
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
if e.enabled and event.x ~= dec_point_x then
local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua
index bf138f2..133ea45 100644
--- a/graphics/elements/controls/switch_button.lua
+++ b/graphics/elements/controls/switch_button.lua
@@ -62,10 +62,10 @@ local function switch_button(args)
-- initial draw
draw_state()
- -- handle touch
- ---@param event monitor_touch monitor touch event
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
---@diagnostic disable-next-line: unused-local
- function e.handle_touch(event)
+ function e.handle_mouse(event)
if e.enabled then
-- toggle state
e.value = not e.value
diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua
new file mode 100644
index 0000000..7905848
--- /dev/null
+++ b/graphics/elements/indicators/led.lua
@@ -0,0 +1,98 @@
+-- Indicator "LED" Graphics Element
+
+local util = require("scada-common.util")
+
+local element = require("graphics.element")
+local flasher = require("graphics.flasher")
+
+---@class indicator_led_args
+---@field label string indicator label
+---@field colors cpair on/off colors (a/b respectively)
+---@field min_label_width? integer label length if omitted
+---@field flash? boolean whether to flash on true rather than stay on
+---@field period? PERIOD flash period
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer 1 if omitted
+---@field fg_bg? cpair foreground/background colors
+
+-- new indicator LED
+---@nodiscard
+---@param args indicator_led_args
+---@return graphics_element element, element_id id
+local function indicator_led(args)
+ assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
+ assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
+
+ if args.flash then
+ assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled")
+ end
+
+ -- single line
+ args.height = 1
+
+ -- determine width
+ args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
+
+ -- flasher state
+ local flash_on = true
+
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ -- called by flasher when enabled
+ local function flash_callback()
+ e.window.setCursorPos(1, 1)
+
+ if flash_on then
+ e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
+ else
+ e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
+ end
+
+ flash_on = not flash_on
+ end
+
+ -- enable light or start flashing
+ local function enable()
+ if args.flash then
+ flash_on = true
+ flasher.start(flash_callback, args.period)
+ else
+ e.window.setCursorPos(1, 1)
+ e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
+ end
+ end
+
+ -- disable light or stop flashing
+ local function disable()
+ if args.flash then
+ flash_on = false
+ flasher.stop(flash_callback)
+ end
+
+ e.window.setCursorPos(1, 1)
+ e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
+ end
+
+ -- on state change
+ ---@param new_state boolean indicator state
+ function e.on_update(new_state)
+ e.value = new_state
+ if new_state then enable() else disable() end
+ end
+
+ -- set indicator state
+ ---@param val boolean indicator state
+ function e.set_value(val) e.on_update(val) end
+
+ -- write label and initial indicator light
+ e.on_update(false)
+ e.window.setCursorPos(3, 1)
+ e.window.write(args.label)
+
+ return e.get()
+end
+
+return indicator_led
diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua
new file mode 100644
index 0000000..aaf94ec
--- /dev/null
+++ b/graphics/elements/indicators/ledpair.lua
@@ -0,0 +1,112 @@
+-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b)
+
+local util = require("scada-common.util")
+
+local element = require("graphics.element")
+local flasher = require("graphics.flasher")
+
+---@class indicator_led_pair_args
+---@field label string indicator label
+---@field off color color for off
+---@field c1 color color for #1 on
+---@field c2 color color for #2 on
+---@field min_label_width? integer label length if omitted
+---@field flash? boolean whether to flash when on rather than stay on
+---@field period? PERIOD flash period
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer 1 if omitted
+---@field fg_bg? cpair foreground/background colors
+
+-- new dual LED indicator light
+---@nodiscard
+---@param args indicator_led_pair_args
+---@return graphics_element element, element_id id
+local function indicator_led_pair(args)
+ assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
+ assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
+ assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
+ assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
+
+ if args.flash then
+ assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled")
+ end
+
+ -- single line
+ args.height = 1
+
+ -- determine width
+ args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
+
+ -- flasher state
+ local flash_on = true
+
+ -- blit translations
+ local co = colors.toBlit(args.off)
+ local c1 = colors.toBlit(args.c1)
+ local c2 = colors.toBlit(args.c2)
+
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ -- init value for initial check in on_update
+ e.value = 1
+
+ -- called by flasher when enabled
+ local function flash_callback()
+ e.window.setCursorPos(1, 1)
+
+ if flash_on then
+ if e.value == 2 then
+ e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
+ elseif e.value == 3 then
+ e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
+ end
+ else
+ e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
+ end
+
+ flash_on = not flash_on
+ end
+
+ -- on state change
+ ---@param new_state integer indicator state
+ function e.on_update(new_state)
+ local was_off = e.value <= 1
+
+ e.value = new_state
+ e.window.setCursorPos(1, 1)
+
+ if args.flash then
+ if was_off and (new_state > 1) then
+ flash_on = true
+ flasher.start(flash_callback, args.period)
+ elseif new_state <= 1 then
+ flash_on = false
+ flasher.stop(flash_callback)
+
+ e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
+ end
+ elseif new_state == 2 then
+ e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
+ elseif new_state == 3 then
+ e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
+ else
+ e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
+ end
+ end
+
+ -- set indicator state
+ ---@param val integer indicator state
+ function e.set_value(val) e.on_update(val) end
+
+ -- write label and initial indicator light
+ e.on_update(1)
+ e.window.setCursorPos(3, 1)
+ e.window.write(args.label)
+
+ return e.get()
+end
+
+return indicator_led_pair
diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua
new file mode 100644
index 0000000..c58b835
--- /dev/null
+++ b/graphics/elements/indicators/ledrgb.lua
@@ -0,0 +1,57 @@
+-- Indicator RGB LED Graphics Element
+
+local element = require("graphics.element")
+
+---@class indicator_led_rgb_args
+---@field label string indicator label
+---@field colors table colors to use
+---@field min_label_width? integer label length if omitted
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer 1 if omitted
+---@field fg_bg? cpair foreground/background colors
+
+-- new RGB LED indicator light
+---@nodiscard
+---@param args indicator_led_rgb_args
+---@return graphics_element element, element_id id
+local function indicator_led_rgb(args)
+ assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
+ assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
+
+ -- single line
+ args.height = 1
+
+ -- determine width
+ args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
+
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ -- init value for initial check in on_update
+ e.value = 1
+
+ -- on state change
+ ---@param new_state integer indicator state
+ function e.on_update(new_state)
+ e.value = new_state
+ e.window.setCursorPos(1, 1)
+ if type(args.colors[new_state]) == "number" then
+ e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
+ end
+ end
+
+ -- set indicator state
+ ---@param val integer indicator state
+ function e.set_value(val) e.on_update(val) end
+
+ -- write label and initial indicator light
+ e.on_update(1)
+ e.window.setCursorPos(3, 1)
+ e.window.write(args.label)
+
+ return e.get()
+end
+
+return indicator_led_rgb
diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua
index 6422cbc..a4d8dde 100644
--- a/graphics/elements/rectangle.lua
+++ b/graphics/elements/rectangle.lua
@@ -7,6 +7,7 @@ local element = require("graphics.element")
---@class rectangle_args
---@field border? graphics_border
---@field thin? boolean true to use extra thin even borders
+---@field even_inner? boolean true to make the inner area of a border even
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
@@ -66,14 +67,27 @@ local function rectangle(args)
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
-- partial bars
- local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
- local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
- local p_s = spaces
-
+ local p_a, p_b, p_s
if args.thin == true then
- p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
- p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
+ if args.even_inner == true then
+ p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93"
+ p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e"
+ else
+ p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
+ p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
+ end
+
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
+ else
+ if args.even_inner == true then
+ p_a = util.strrep("\x83", inner_width + width_x2)
+ p_b = util.strrep("\x8f", inner_width + width_x2)
+ else
+ p_a = util.spaces(border_width) .. util.strrep("\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)
diff --git a/imgen.py b/imgen.py
index 8b61d7d..30e7e69 100644
--- a/imgen.py
+++ b/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" ],
diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua
new file mode 100644
index 0000000..eeb2260
--- /dev/null
+++ b/reactor-plc/databus.lua
@@ -0,0 +1,101 @@
+--
+-- Data Bus - Central Communication Linking for PLC Front Panel
+--
+
+local log = require("scada-common.log")
+local psil = require("scada-common.psil")
+local util = require("scada-common.util")
+
+local databus = {}
+
+local dbus_iface = {
+ ps = psil.create(),
+ rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
+ rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
+}
+
+-- call to toggle heartbeat signal
+function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
+
+-- link RPS command functions
+---@param scram function reactor SCRAM function
+---@param reset function RPS reset function
+function databus.link_rps(scram, reset)
+ dbus_iface.rps_scram = scram
+ dbus_iface.rps_reset = reset
+end
+
+-- transmit a command to the RPS to SCRAM
+function databus.rps_scram() dbus_iface.rps_scram() end
+
+-- transmit a command to the RPS to reset
+function databus.rps_reset() dbus_iface.rps_reset() end
+
+-- transmit firmware versions across the bus
+---@param plc_v string PLC version
+---@param comms_v string comms version
+function databus.tx_versions(plc_v, comms_v)
+ dbus_iface.ps.publish("version", plc_v)
+ dbus_iface.ps.publish("comms_version", comms_v)
+end
+
+-- transmit unit ID across the bus
+---@param id integer unit ID
+function databus.tx_id(id)
+ dbus_iface.ps.publish("unit_id", id)
+end
+
+-- transmit hardware status across the bus
+---@param plc_state plc_state
+function databus.tx_hw_status(plc_state)
+ dbus_iface.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
+ dbus_iface.ps.publish("has_modem", not plc_state.no_modem)
+ dbus_iface.ps.publish("degraded", plc_state.degraded)
+ dbus_iface.ps.publish("init_ok", plc_state.init_ok)
+end
+
+-- transmit thread (routine) statuses
+---@param thread string thread name
+---@param ok boolean thread state
+function databus.tx_rt_status(thread, ok)
+ dbus_iface.ps.publish(util.c("routine__", thread), ok)
+end
+
+-- transmit supervisor link state across the bus
+---@param state integer
+function databus.tx_link_state(state)
+ dbus_iface.ps.publish("link_state", state)
+end
+
+-- transmit reactor enable state across the bus
+---@param active boolean reactor active
+function databus.tx_reactor_state(active)
+ dbus_iface.ps.publish("reactor_active", active)
+end
+
+-- transmit RPS data across the bus
+---@param tripped boolean RPS tripped
+---@param status table RPS status
+function databus.tx_rps(tripped, status)
+ dbus_iface.ps.publish("rps_scram", tripped)
+ dbus_iface.ps.publish("rps_damage", status[1])
+ dbus_iface.ps.publish("rps_high_temp", status[2])
+ dbus_iface.ps.publish("rps_low_ccool", status[3])
+ dbus_iface.ps.publish("rps_high_waste", status[4])
+ dbus_iface.ps.publish("rps_high_hcool", status[5])
+ dbus_iface.ps.publish("rps_no_fuel", status[6])
+ dbus_iface.ps.publish("rps_fault", status[7])
+ dbus_iface.ps.publish("rps_timeout", status[8])
+ dbus_iface.ps.publish("rps_manual", status[9])
+ dbus_iface.ps.publish("rps_automatic", status[10])
+ dbus_iface.ps.publish("rps_sysfail", status[11])
+end
+
+-- link a function to receive data from the bus
+---@param field string field name
+---@param func function function to link
+function databus.rx_field(field, func)
+ dbus_iface.ps.subscribe(field, func)
+end
+
+return databus
diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua
new file mode 100644
index 0000000..f875b5d
--- /dev/null
+++ b/reactor-plc/panel/front_panel.lua
@@ -0,0 +1,124 @@
+--
+-- Main SCADA Coordinator GUI
+--
+
+local util = require("scada-common.util")
+
+local databus = require("reactor-plc.databus")
+
+local style = require("reactor-plc.panel.style")
+
+local core = require("graphics.core")
+local flasher = require("graphics.flasher")
+
+local DisplayBox = require("graphics.elements.displaybox")
+local Div = require("graphics.elements.div")
+local Rectangle = require("graphics.elements.rectangle")
+local TextBox = require("graphics.elements.textbox")
+
+local PushButton = require("graphics.elements.controls.push_button")
+
+local LED = require("graphics.elements.indicators.led")
+local LEDPair = require("graphics.elements.indicators.ledpair")
+local RGBLED = require("graphics.elements.indicators.ledrgb")
+
+local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+
+local cpair = core.graphics.cpair
+local border = core.graphics.border
+
+-- create new main view
+---@param monitor table main viewscreen
+local function init(monitor)
+ local panel = DisplayBox{window=monitor,fg_bg=style.root}
+
+ local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
+ databus.rx_field("unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
+
+ local system = Div{parent=panel,width=14,height=18,x=2,y=3}
+
+ local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
+ local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
+ system.line_break()
+
+ databus.rx_field("init_ok", init_ok.update)
+ databus.rx_field("heartbeat", heartbeat.update)
+
+ local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
+ local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
+ local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
+ network.update(5)
+ system.line_break()
+
+ databus.rx_field("reactor_dev_state", reactor.update)
+ databus.rx_field("has_modem", modem.update)
+ databus.rx_field("link_state", network.update)
+
+ local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
+ local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
+ local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)}
+ local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.green_off)}
+ local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)}
+ system.line_break()
+
+ databus.rx_field("routine__main", rt_main.update)
+ databus.rx_field("routine__rps", rt_rps.update)
+ databus.rx_field("routine__comms_tx", rt_cmtx.update)
+ databus.rx_field("routine__comms_rx", rt_cmrx.update)
+ databus.rx_field("routine__spctl", rt_sctl.update)
+
+ local status = Div{parent=panel,width=19,height=18,x=17,y=3}
+
+ local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)}
+
+ local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,y=2,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
+ local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS}
+
+ local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,y=5,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
+ local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)}
+ PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
+ PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
+
+ databus.rx_field("reactor_active", active.update)
+ databus.rx_field("rps_scram", scram.update)
+
+ local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)}
+ local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
+ local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
+
+ databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
+ databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
+
+ local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
+ local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)}
+ local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)}
+ local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)}
+ local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)}
+ local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)}
+ rps.line_break()
+ local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)}
+ local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)}
+ rps.line_break()
+ local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)}
+ local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)}
+ rps.line_break()
+ local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)}
+ local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)}
+
+ databus.rx_field("rps_manual", rps_man.update)
+ databus.rx_field("rps_automatic", rps_auto.update)
+ databus.rx_field("rps_timeout", rps_tmo.update)
+ databus.rx_field("rps_fault", rps_flt.update)
+ databus.rx_field("rps_sysfail", rps_fail.update)
+ databus.rx_field("rps_damage", rps_dmg.update)
+ databus.rx_field("rps_high_temp", rps_tmp.update)
+ databus.rx_field("rps_no_fuel", rps_nof.update)
+ databus.rx_field("rps_high_waste", rps_wst.update)
+ databus.rx_field("rps_low_ccool", rps_ccl.update)
+ databus.rx_field("rps_high_hcool", rps_hcl.update)
+
+ return panel
+end
+
+return init
diff --git a/reactor-plc/panel/style.lua b/reactor-plc/panel/style.lua
new file mode 100644
index 0000000..01b00c9
--- /dev/null
+++ b/reactor-plc/panel/style.lua
@@ -0,0 +1,41 @@
+--
+-- Graphics Style Options
+--
+
+local core = require("graphics.core")
+
+local style = {}
+
+local cpair = core.graphics.cpair
+
+-- GLOBAL --
+
+-- remap global colors
+colors.ivory = colors.pink
+colors.red_off = colors.brown
+colors.yellow_off = colors.magenta
+colors.green_off = colors.lime
+
+style.root = cpair(colors.black, colors.ivory)
+style.header = cpair(colors.black, colors.lightGray)
+
+style.colors = {
+ { c = colors.red, hex = 0xdf4949 }, -- RED ON
+ { c = colors.orange, hex = 0xffb659 },
+ { c = colors.yellow, hex = 0xf9fb53 },
+ { c = colors.lime, hex = 0x16665a }, -- GREEN OFF
+ { c = colors.green, hex = 0x6be551 }, -- GREEN ON
+ { c = colors.cyan, hex = 0x34bac8 },
+ { c = colors.lightBlue, hex = 0x6cc0f2 },
+ { c = colors.blue, hex = 0x0096ff },
+ { c = colors.purple, hex = 0xb156ee },
+ { c = colors.pink, hex = 0xdcd9ca }, -- IVORY
+ { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
+ -- { c = colors.white, hex = 0xdcd9ca },
+ { c = colors.lightGray, hex = 0xb1b8b3 },
+ { c = colors.gray, hex = 0x575757 },
+ -- { c = colors.black, hex = 0x191919 },
+ { c = colors.brown, hex = 0x672223 } -- RED OFF
+}
+
+return style
diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua
index 6c2b066..2879290 100644
--- a/reactor-plc/plc.lua
+++ b/reactor-plc/plc.lua
@@ -1,10 +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 rsio = require("scada-common.rsio")
-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 = {}
@@ -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
diff --git a/reactor-plc/renderer.lua b/reactor-plc/renderer.lua
new file mode 100644
index 0000000..ad2bcc0
--- /dev/null
+++ b/reactor-plc/renderer.lua
@@ -0,0 +1,75 @@
+--
+-- Graphics Rendering Control
+--
+
+local style = require("reactor-plc.panel.style")
+local panel_view = require("reactor-plc.panel.front_panel")
+
+local flasher = require("graphics.flasher")
+
+local renderer = {}
+
+local ui = {
+ view = nil
+}
+
+-- start the UI
+function renderer.start_ui()
+ if ui.view == nil then
+ -- reset terminal
+ term.setTextColor(colors.white)
+ term.setBackgroundColor(colors.black)
+ term.clear()
+ term.setCursorPos(1, 1)
+
+ -- set overridden colors
+ for i = 1, #style.colors do
+ term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
+ end
+
+ -- start flasher callback task
+ flasher.run()
+
+ -- init front panel view
+ ui.view = panel_view(term.current())
+ end
+end
+
+-- close out the UI
+function renderer.close_ui()
+ -- stop blinking indicators
+ flasher.clear()
+
+ if ui.view ~= nil then
+ -- hide to stop animation callbacks
+ ui.view.hide()
+ end
+
+ -- clear root UI elements
+ ui.view = nil
+
+ -- restore colors
+ for i = 1, #style.colors do
+ local r, g, b = term.nativePaletteColor(style.colors[i].c)
+ term.setPaletteColor(style.colors[i].c, r, g, b)
+ end
+
+ -- reset terminal
+ term.setTextColor(colors.white)
+ term.setBackgroundColor(colors.black)
+ term.clear()
+ term.setCursorPos(1, 1)
+end
+
+-- is the UI ready?
+---@nodiscard
+---@return boolean ready
+function renderer.ui_ready() return ui.view ~= nil end
+
+-- handle a mouse event
+---@param event mouse_interaction
+function renderer.handle_mouse(event)
+ ui.view.handle_mouse(event)
+end
+
+return renderer
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index dc9d3ce..293281c 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -4,18 +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 rsio = require("scada-common.rsio")
-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.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
--- 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, 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
diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua
index c288281..c307999 100644
--- a/reactor-plc/threads.lua
+++ b/reactor-plc/threads.lua
@@ -1,15 +1,16 @@
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local ppm = require("scada-common.ppm")
-local util = require("scada-common.util")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local ppm = require("scada-common.ppm")
+local tcallbackdsp = require("scada-common.tcallbackdsp")
+local util = require("scada-common.util")
+
+local databus = require("reactor-plc.databus")
+local renderer = require("reactor-plc.renderer")
+
+local core = require("graphics.core")
local threads = {}
-local print = util.print
-local println = util.println
-local print_ts = util.print_ts
-local println_ts = util.println_ts
-
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RPS_SLEEP = 250 -- (250ms, 5 ticks)
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
@@ -32,11 +33,16 @@ local MQ__COMM_CMD = {
---@param smem plc_shared_memory
---@param init function
function threads.thread__main(smem, init)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end
+ local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
+
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
+ databus.tx_rt_status("main", true)
log.debug("main thread init, clock inactive")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
@@ -61,6 +67,9 @@ function threads.thread__main(smem, init)
-- handle event
if event == "timer" and loop_clock.is_clock(param1) then
+ -- blink heartbeat indicator
+ databus.heartbeat()
+
-- core clock tick
if networked then
-- start next clock timer
@@ -133,6 +142,9 @@ function threads.thread__main(smem, init)
-- reactor no longer formed
plc_state.reactor_formed = false
end
+
+ -- update indicators
+ databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then
-- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
@@ -144,6 +156,9 @@ function threads.thread__main(smem, init)
-- haven't heard from server recently? shutdown reactor
plc_comms.unlink()
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
+ elseif event == "timer" then
+ -- notify timer callback dispatcher if no other timer case claimed this event
+ tcallbackdsp.handle(param1)
elseif event == "peripheral_detach" then
-- peripheral disconnect
local type, device = ppm.handle_unmount(param1)
@@ -174,6 +189,9 @@ function threads.thread__main(smem, init)
end
end
end
+
+ -- update indicators
+ databus.tx_hw_status(plc_state)
elseif event == "peripheral" then
-- peripheral connect
local type, device = ppm.mount(param1)
@@ -237,6 +255,12 @@ function threads.thread__main(smem, init)
plc_state.init_ok = true
init()
end
+
+ -- update indicators
+ databus.tx_hw_status(plc_state)
+ elseif event == "mouse_click" then
+ -- handle a monitor touch event
+ renderer.handle_mouse(core.events.click(param1, param2, param3))
elseif event == "clock_start" then
-- start loop clock
loop_clock.start()
@@ -263,6 +287,8 @@ function threads.thread__main(smem, init)
log.fatal(util.strval(result))
end
+ databus.tx_rt_status("main", false)
+
-- if status is true, then we are probably exiting, so this won't matter
-- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
@@ -280,11 +306,16 @@ end
---@nodiscard
---@param smem plc_shared_memory
function threads.thread__rps(smem)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end
+ local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
+
---@class parallel_thread
local public = {}
-- execute thread
function public.exec()
+ databus.tx_rt_status("rps", true)
log.debug("rps thread start")
-- load in from shared memory
@@ -317,11 +348,17 @@ function threads.thread__rps(smem)
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
@@ -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)
diff --git a/scada-common/log.lua b/scada-common/log.lua
index 424bf55..30f785d 100644
--- a/scada-common/log.lua
+++ b/scada-common/log.lua
@@ -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",
diff --git a/scada-common/psil.lua b/scada-common/psil.lua
index c21b2cf..664d10d 100644
--- a/scada-common/psil.lua
+++ b/scada-common/psil.lua
@@ -51,6 +51,19 @@ function psil.create()
self.ic[key].value = value
end
+ -- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed
+ -- this is intended to be used to toggle boolean indicators such as heartbeats without extra state variables
+ ---@param key string data key
+ function public.toggle(key)
+ if self.ic[key] == nil then alloc(key) end
+
+ self.ic[key].value = self.ic[key].value == false
+
+ for i = 1, #self.ic[key].subscribers do
+ self.ic[key].subscribers[i].notify(self.ic[key].value)
+ end
+ end
+
return public
end