diff --git a/coordinator/startup.lua b/coordinator/startup.lua
index 367c1ca..c0addf1 100644
--- a/coordinator/startup.lua
+++ b/coordinator/startup.lua
@@ -20,7 +20,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions")
-local COORDINATOR_VERSION = "v0.15.2"
+local COORDINATOR_VERSION = "v0.15.6"
local println = util.println
local println_ts = util.println_ts
diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua
index a238a8c..7e0016a 100644
--- a/coordinator/ui/components/processctl.lua
+++ b/coordinator/ui/components/processctl.lua
@@ -33,7 +33,7 @@ local period = core.flasher.PERIOD
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
- assert(root.height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
+ assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua
index bd341bf..3af8c83 100644
--- a/coordinator/ui/components/unit_overview.lua
+++ b/coordinator/ui/components/unit_overview.lua
@@ -38,7 +38,7 @@ local function make(parent, x, y, unit)
height = 17
end
- assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
+ assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua
index a758b24..a7b3c86 100644
--- a/coordinator/ui/layout/main_view.lua
+++ b/coordinator/ui/layout/main_view.lua
@@ -32,7 +32,7 @@ local function init(main)
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
- local datetime = TextBox{parent=main,x=(header.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
+ local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value)
@@ -45,12 +45,12 @@ local function init(main)
-- unit overviews
if facility.num_units >= 1 then
uo_1 = unit_overview(main, 2, 3, units[1])
- row_1_height = uo_1.height()
+ row_1_height = uo_1.get_height()
end
if facility.num_units >= 2 then
uo_2 = unit_overview(main, 84, 3, units[2])
- row_1_height = math.max(row_1_height, uo_2.height())
+ row_1_height = math.max(row_1_height, uo_2.get_height())
end
cnc_y_start = cnc_y_start + row_1_height + 1
@@ -60,11 +60,11 @@ local function init(main)
local row_2_offset = cnc_y_start
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
- cnc_y_start = row_2_offset + uo_3.height() + 1
+ cnc_y_start = row_2_offset + uo_3.get_height() + 1
if facility.num_units == 4 then
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
- cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.height() + 1)
+ cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
end
end
@@ -73,11 +73,11 @@ local function init(main)
cnc_y_start = cnc_y_start
-- induction matrix and process control interfaces are 24 tall + space needed for divider
- local cnc_bottom_align_start = main.height() - 26
+ local cnc_bottom_align_start = main.get_height() - 26
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
- TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
+ TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
cnc_bottom_align_start = cnc_bottom_align_start + 2
diff --git a/graphics/element.lua b/graphics/element.lua
index b865af0..ae2daee 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -12,12 +12,11 @@ local element = {}
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer next line if omitted
----@field offset_x? integer 0 if omitted
----@field offset_y? integer 0 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
---@alias graphics_args graphics_args_generic
---|waiting_args
@@ -46,6 +45,7 @@ local element = {}
---|colormap_args
---|displaybox_args
---|div_args
+---|listbox_args
---|multipane_args
---|pipenet_args
---|rectangle_args
@@ -62,26 +62,26 @@ local element = {}
---@param args graphics_args arguments
function element.new(args)
local self = {
- id = -1,
+ id = nil, ---@type element_id|nil
elem_type = debug.getinfo(2).name,
define_completed = false,
p_window = nil, ---@type table
position = { x = 1, y = 1 }, ---@type coordinate_2d
- child_offset = { x = 0, y = 0 },
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
next_y = 1,
- children = {},
subscriptions = {},
mt = {}
}
- ---@class graphics_template
+ ---@class graphics_base
local protected = {
enabled = true,
- value = nil, ---@type any
- window = nil, ---@type table
+ value = nil, ---@type any
+ window = nil, ---@type table
+ content_window = nil, ---@type table|nil
fg_bg = core.cpair(colors.white, colors.black),
- frame = core.gframe(1, 1, 1, 1)
+ frame = core.gframe(1, 1, 1, 1),
+ children = {}
}
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
@@ -101,10 +101,8 @@ function element.new(args)
-------------------------
-- prepare the template
- ---@param offset_x integer x offset
- ---@param offset_y integer y offset
---@param next_y integer next line if no y was provided
- function protected.prepare_template(offset_x, offset_y, next_y)
+ function protected.prepare_template(next_y)
-- get frame coordinates/size
if args.gframe ~= nil then
protected.frame.x = args.gframe.x
@@ -114,36 +112,18 @@ 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.y = args.y or next_y
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
- -- inner offsets
- if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
- if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
-
-- adjust window frame if applicable
local f = protected.frame
- local x = f.x
- local y = f.y
-
- -- apply offsets
if args.parent ~= nil then
-- constrain to parent inner width/height
local w, h = self.p_window.getSize()
- f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1)))
- f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1)))
-
- -- offset x/y
- f.x = x + offset_x
- f.y = y + offset_y
+ f.w = math.min(f.w, w - (f.x - 1))
+ f.h = math.min(f.h, h - (f.y - 1))
end
-- check frame
@@ -153,7 +133,7 @@ function element.new(args)
assert(f.h >= 1, name_brief .. "frame height not >= 1")
-- create window
- protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true)
+ protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
-- init colors
if args.fg_bg ~= nil then
@@ -198,15 +178,15 @@ function element.new(args)
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg
- -- dynamically insert a child element
- ---@param id string|integer element identifier
- ---@param elem graphics_element element
- function protected.insert(id, elem)
+ -- handle a child element having been added
+ ---@param id element_id element identifier
+ ---@param child graphics_element child element
+ function protected.on_added(id, child)
end
- -- dynamically remove a child element
- ---@param id string|integer element identifier
- function protected.remove(id)
+ -- handle a child element having been removed
+ ---@param id element_id element identifier
+ function protected.on_removed(id)
end
-- handle a mouse event
@@ -279,6 +259,14 @@ function element.new(args)
---@return graphics_element element, element_id id
function protected.get() return public, self.id end
+ -- report completion of element instantiation and get the public interface
+ ---@nodiscard
+ ---@return graphics_element element, element_id id
+ function protected.complete()
+ if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
+ return public, self.id
+ end
+
-----------
-- SETUP --
-----------
@@ -294,7 +282,8 @@ function element.new(args)
-- prepare the template
if args.parent == nil then
- protected.prepare_template(0, 0, 1)
+ self.id = args.id or "__ROOT__"
+ protected.prepare_template(1)
else
self.id = args.parent.__add_child(args.id, protected)
end
@@ -305,11 +294,21 @@ function element.new(args)
-- get the window object
---@nodiscard
- function public.window() return protected.window end
+ function public.window() return protected.content_window or protected.window end
-- delete this element (hide and unsubscribe from PSIL)
function public.delete()
- -- hide + stop animations
+ local fg_bg = protected.fg_bg
+
+ if args.parent ~= nil then
+ -- grab parent fg/bg so we can clear cleanly as a child element
+ fg_bg = args.parent.get_fg_bg()
+ end
+
+ -- clear, hide, and stop animations
+ protected.window.setBackgroundColor(fg_bg.bkg)
+ protected.window.setTextColor(fg_bg.fgd)
+ protected.window.clear()
public.hide()
-- unsubscribe from PSIL
@@ -319,9 +318,14 @@ function element.new(args)
end
-- delete all children
- for k, v in pairs(self.children) do
+ for k, v in pairs(protected.children) do
v.delete()
- self.children[k] = nil
+ protected.children[k] = nil
+ end
+
+ if args.parent ~= nil then
+ -- remove self from parent
+ args.parent.__remove_child(self.id)
end
end
@@ -330,41 +334,53 @@ function element.new(args)
-- add a child element
---@nodiscard
---@param key string|nil id
- ---@param child graphics_template
+ ---@param child graphics_base
---@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)
+ child.prepare_template(self.next_y)
self.next_y = child.frame.y + child.frame.h
local child_element = child.get()
if key == nil then
- table.insert(self.children, child_element)
- return #self.children
+ table.insert(protected.children, child_element)
+ return #protected.children
else
- self.children[key] = child_element
+ protected.children[key] = child_element
return key
end
end
+ -- remove a child element
+ ---@param key element_id id
+ function public.__remove_child(key)
+ if protected.children[key] ~= nil then
+ protected.on_removed(key)
+ protected.children[key] = nil
+ end
+ end
+
+ -- actions to take upon a child element becoming ready (initial draw/construction completed)
+ ---@param key element_id id
+ ---@param child graphics_element
+ function public.__child_ready(key, child)
+ protected.on_added(key, child)
+ end
+
-- get a child element
---@nodiscard
---@param id element_id
---@return graphics_element
- function public.get_child(id) return self.children[id] end
+ function public.get_child(id) return protected.children[id] end
-- remove a child element
---@param id element_id
function public.remove(id)
- if self.children[id] ~= nil then
- self.children[id].delete()
- self.children[id] = nil
+ if protected.children[id] ~= nil then
+ protected.children[id].delete()
+ protected.on_removed(id)
+ protected.children[id] = nil
end
end
@@ -373,37 +389,18 @@ function element.new(args)
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
- if self.children[id] == nil then
- for _, child in pairs(self.children) do
+ if protected.children[id] == nil then
+ for _, child in pairs(protected.children) do
local elem = child.get_element_by_id(id)
if elem ~= nil then return elem end
end
else
- return self.children[id]
+ return protected.children[id]
end
return nil
end
- -- DYNAMIC CHILD ELEMENTS --
-
- -- insert an element as a contained child
- -- this is intended to be used dynamically, and depends on the target element type.
- -- not all elements support dynamic children.
- ---@param id string|integer element identifier
- ---@param elem graphics_element element
- function public.insert_element(id, elem)
- protected.insert(id, elem)
- end
-
- -- remove an element from contained children
- -- this is intended to be used dynamically, and depends on the target element type.
- -- not all elements support dynamic children.
- ---@param id string|integer element identifier
- function public.remove_element(id)
- protected.remove(id)
- end
-
-- AUTO-PLACEMENT --
-- skip a line for automatically placed elements
@@ -437,14 +434,14 @@ function element.new(args)
-- get element width
---@nodiscard
---@return integer width
- function public.width()
+ function public.get_width()
return protected.frame.w
end
-- get element height
---@nodiscard
---@return integer height
- function public.height()
+ function public.get_height()
return protected.frame.h
end
@@ -519,7 +516,7 @@ function element.new(args)
-- handle the mouse event then pass to children
protected.handle_mouse(event_T)
- for _, child in pairs(self.children) do child.handle_mouse(event_T) end
+ for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
end
end
@@ -545,27 +542,61 @@ function element.new(args)
ps.subscribe(key, func)
end
- -- VISIBILITY --
+ -- VISIBILITY & ANIMATIONS --
- -- show the element
- function public.show()
+ -- show the element and enables animations by default
+ ---@param animate? boolean true (default) to automatically resume animations
+ function public.show(animate)
protected.window.setVisible(true)
- protected.start_anim()
- for _, child in pairs(self.children) do child.show() end
+ if animate ~= false then public.animate_all() end
end
- -- hide the element
+ -- hide the element and disables animations
+ -- this alone does not cause an element to be fully hidden, it only prevents updates from being shown
+ ---@see graphics_element.content_redraw
function public.hide()
- protected.stop_anim()
- for _, child in pairs(self.children) do child.hide() end
+ public.freeze_all() -- stop animations for efficiency/performance
protected.window.setVisible(false)
end
+ -- start/resume animation(s)
+ function public.animate()
+ protected.start_anim()
+ end
+
+ -- start/resume animation(s) for this element and all its children
+ -- only animates if a window is visible
+ function public.animate_all()
+ if protected.window.isVisible() then
+ public.animate()
+ for _, child in pairs(protected.children) do child.animate_all() end
+ end
+ end
+
+ -- freeze animation(s)
+ function public.freeze()
+ protected.stop_anim()
+ end
+
+ -- freeze animation(s) for this element and all its children
+ function public.freeze_all()
+ public.freeze()
+ for _, child in pairs(protected.children) do child.freeze_all() end
+ end
+
-- re-draw the element
function public.redraw()
protected.window.redraw()
end
+ -- if a content window is set, clears it then re-draws all children
+ function public.content_redraw()
+ if protected.content_window ~= nil then
+ protected.content_window.clear()
+ for _, child in pairs(protected.children) do child.redraw() end
+ end
+ end
+
return protected
end
diff --git a/graphics/elements/animations/waiting.lua b/graphics/elements/animations/waiting.lua
index a0d7b3e..9dc089f 100644
--- a/graphics/elements/animations/waiting.lua
+++ b/graphics/elements/animations/waiting.lua
@@ -10,6 +10,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new waiting animation element
---@param args waiting_args
@@ -102,7 +103,7 @@ local function waiting(args)
e.start_anim()
- return e.get()
+ return e.complete()
end
return waiting
diff --git a/graphics/elements/colormap.lua b/graphics/elements/colormap.lua
index 4c7ba94..be92d83 100644
--- a/graphics/elements/colormap.lua
+++ b/graphics/elements/colormap.lua
@@ -9,6 +9,7 @@ local element = require("graphics.element")
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
+---@field hidden? boolean true to hide on initial draw
-- new color map
---@param args colormap_args
@@ -27,7 +28,7 @@ local function colormap(args)
e.window.setCursorPos(1, 1)
e.window.blit(spaces, bkg, bkg)
- return e.get()
+ return e.complete()
end
return colormap
diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua
index 4dca5c4..6ffe74d 100644
--- a/graphics/elements/controls/hazard_button.lua
+++ b/graphics/elements/controls/hazard_button.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new hazard button
---@param args hazard_button_args
@@ -198,7 +199,7 @@ local function hazard_button(args)
-- initial draw of border
draw_border(args.accent)
- return e.get()
+ return e.complete()
end
return hazard_button
diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua
index e44bad0..279c9a7 100644
--- a/graphics/elements/controls/multi_button.lua
+++ b/graphics/elements/controls/multi_button.lua
@@ -23,6 +23,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args
@@ -130,7 +131,7 @@ local function multi_button(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return multi_button
diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua
index 27be991..564ad3c 100644
--- a/graphics/elements/controls/push_button.lua
+++ b/graphics/elements/controls/push_button.lua
@@ -19,6 +19,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new push button
---@param args push_button_args
@@ -120,7 +121,7 @@ local function push_button(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return push_button
diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua
index 050bf39..e3edf24 100644
--- a/graphics/elements/controls/radio_button.lua
+++ b/graphics/elements/controls/radio_button.lua
@@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new radio button list (latch selection, exclusively one button at a time)
---@param args radio_button_args
@@ -103,7 +104,7 @@ local function radio_button(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return radio_button
diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua
index 45771b2..b3221b3 100644
--- a/graphics/elements/controls/sidebar.lua
+++ b/graphics/elements/controls/sidebar.lua
@@ -20,6 +20,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new sidebar tab selector
---@param args sidebar_args
@@ -115,7 +116,7 @@ local function sidebar(args)
-- initial draw
draw(false)
- return e.get()
+ return e.complete()
end
return sidebar
diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua
index 6b88c0e..767d97b 100644
--- a/graphics/elements/controls/spinbox_numeric.lua
+++ b/graphics/elements/controls/spinbox_numeric.lua
@@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new spinbox control (minimum value is 0)
---@param args spinbox_args
@@ -188,7 +189,7 @@ local function spinbox(args)
e.value = 0
set_digits()
- return e.get()
+ return e.complete()
end
return spinbox
diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua
index 645bf8a..6d2e09c 100644
--- a/graphics/elements/controls/switch_button.lua
+++ b/graphics/elements/controls/switch_button.lua
@@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new switch button (latch high/low)
---@param args switch_button_args
@@ -86,7 +87,7 @@ local function switch_button(args)
draw_state()
end
- return e.get()
+ return e.complete()
end
return switch_button
diff --git a/graphics/elements/controls/tabbar.lua b/graphics/elements/controls/tabbar.lua
index 6249951..da4738b 100644
--- a/graphics/elements/controls/tabbar.lua
+++ b/graphics/elements/controls/tabbar.lua
@@ -21,6 +21,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new tab selector
---@param args tabbar_args
@@ -124,7 +125,7 @@ local function tabbar(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return tabbar
diff --git a/graphics/elements/displaybox.lua b/graphics/elements/displaybox.lua
index c7e5c9f..3578a63 100644
--- a/graphics/elements/displaybox.lua
+++ b/graphics/elements/displaybox.lua
@@ -4,19 +4,22 @@ local element = require("graphics.element")
---@class displaybox_args
---@field window table
+---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new root display box
---@nodiscard
---@param args displaybox_args
+---@return graphics_element element, element_id id
local function displaybox(args)
-- create new graphics element base object
- return element.new(args).get()
+ return element.new(args).complete()
end
return displaybox
diff --git a/graphics/elements/div.lua b/graphics/elements/div.lua
index 5eeef71..4b6bd6a 100644
--- a/graphics/elements/div.lua
+++ b/graphics/elements/div.lua
@@ -11,6 +11,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new div element
---@nodiscard
@@ -18,7 +19,7 @@ local element = require("graphics.element")
---@return graphics_element element, element_id id
local function div(args)
-- create new graphics element base object
- return element.new(args).get()
+ return element.new(args).complete()
end
return div
diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua
index 8bb8fa6..ff9b1ad 100644
--- a/graphics/elements/indicators/alight.lua
+++ b/graphics/elements/indicators/alight.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new alarm indicator light
---@nodiscard
@@ -108,7 +109,7 @@ local function alarm_indicator_light(args)
e.on_update(1)
e.window.write(args.label)
- return e.get()
+ return e.complete()
end
return alarm_indicator_light
diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/coremap.lua
index 05434a3..127a8a3 100644
--- a/graphics/elements/indicators/coremap.lua
+++ b/graphics/elements/indicators/coremap.lua
@@ -163,7 +163,7 @@ local function core_map(args)
-- initial draw
e.on_update(0)
- return e.get()
+ return e.complete()
end
return core_map
diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/data.lua
index 66d45dc..9282a03 100644
--- a/graphics/elements/indicators/data.lua
+++ b/graphics/elements/indicators/data.lua
@@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new data indicator
---@nodiscard
@@ -43,8 +44,9 @@ local function data(args)
e.window.setCursorPos(1, 1)
e.window.write(args.label)
- local label_len = string.len(args.label)
- local data_start = 1
+ local value_color = e.fg_bg.fgd
+ local label_len = string.len(args.label)
+ local data_start = 1
local clear_width = args.width
if label_len > 0 then
@@ -64,7 +66,7 @@ local function data(args)
-- write data
local data_str = util.sprintf(args.format, value)
e.window.setCursorPos(data_start, 1)
- e.window.setTextColor(e.fg_bg.fgd)
+ e.window.setTextColor(value_color)
if args.commas then
e.window.write(util.comma_format(data_str))
else
@@ -84,10 +86,17 @@ local function data(args)
---@param val any new value
function e.set_value(val) e.on_update(val) end
+ -- change the foreground color of the value, or all text if no label/unit colors provided
+ ---@param c color
+ function e.recolor(c)
+ value_color = c
+ e.on_update(e.value)
+ end
+
-- initial value draw
e.on_update(args.value)
- return e.get()
+ return e.complete()
end
return data
diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/hbar.lua
index 2d9b110..9bee59f 100644
--- a/graphics/elements/indicators/hbar.lua
+++ b/graphics/elements/indicators/hbar.lua
@@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new horizontal bar
---@nodiscard
@@ -119,7 +120,7 @@ local function hbar(args)
-- initialize to 0
e.on_update(0)
- return e.get()
+ return e.complete()
end
return hbar
diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua
index f31479d..03c88fb 100644
--- a/graphics/elements/indicators/icon.lua
+++ b/graphics/elements/indicators/icon.lua
@@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new icon indicator
---@nodiscard
@@ -68,7 +69,7 @@ local function icon(args)
-- initial icon draw
e.on_update(args.value or 1)
- return e.get()
+ return e.complete()
end
return icon
diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua
index dd2264e..077cab3 100644
--- a/graphics/elements/indicators/led.lua
+++ b/graphics/elements/indicators/led.lua
@@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new indicator LED
---@nodiscard
@@ -94,7 +95,7 @@ local function indicator_led(args)
e.window.write(args.label)
end
- return e.get()
+ return e.complete()
end
return indicator_led
diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua
index 727d4cd..47c9a0a 100644
--- a/graphics/elements/indicators/ledpair.lua
+++ b/graphics/elements/indicators/ledpair.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new dual LED indicator light
---@nodiscard
@@ -108,7 +109,7 @@ local function indicator_led_pair(args)
e.window.write(args.label)
end
- return e.get()
+ return e.complete()
end
return indicator_led_pair
diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua
index 266e0ab..dbcb947 100644
--- a/graphics/elements/indicators/ledrgb.lua
+++ b/graphics/elements/indicators/ledrgb.lua
@@ -11,6 +11,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new RGB LED indicator light
---@nodiscard
@@ -53,7 +54,7 @@ local function indicator_led_rgb(args)
e.window.write(args.label)
end
- return e.get()
+ return e.complete()
end
return indicator_led_rgb
diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua
index e764ad9..d4e8b09 100644
--- a/graphics/elements/indicators/light.lua
+++ b/graphics/elements/indicators/light.lua
@@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new indicator light
---@nodiscard
@@ -92,7 +93,7 @@ local function indicator_light(args)
e.window.setCursorPos(3, 1)
e.window.write(args.label)
- return e.get()
+ return e.complete()
end
return indicator_light
diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua
index 1d727ae..323fe58 100644
--- a/graphics/elements/indicators/power.lua
+++ b/graphics/elements/indicators/power.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new power indicator
---@nodiscard
@@ -79,7 +80,7 @@ local function power(args)
-- initial value draw
e.on_update(args.value)
- return e.get()
+ return e.complete()
end
return power
diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua
index 2e4ad56..fc89044 100644
--- a/graphics/elements/indicators/rad.lua
+++ b/graphics/elements/indicators/rad.lua
@@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new radiation indicator
---@nodiscard
@@ -84,7 +85,7 @@ local function rad(args)
-- initial value draw
e.on_update(types.new_zero_radiation_reading())
- return e.get()
+ return e.complete()
end
return rad
diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/state.lua
index 10d081b..d0e57b5 100644
--- a/graphics/elements/indicators/state.lua
+++ b/graphics/elements/indicators/state.lua
@@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new state indicator
---@nodiscard
@@ -74,7 +75,7 @@ local function state_indicator(args)
-- initial draw
e.on_update(args.value or 1)
- return e.get()
+ return e.complete()
end
return state_indicator
diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua
index 543ebf5..ef8a8b6 100644
--- a/graphics/elements/indicators/trilight.lua
+++ b/graphics/elements/indicators/trilight.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new tri-state indicator light
---@nodiscard
@@ -105,7 +106,7 @@ local function tristate_indicator_light(args)
e.on_update(1)
e.window.write(args.label)
- return e.get()
+ return e.complete()
end
return tristate_indicator_light
diff --git a/graphics/elements/indicators/vbar.lua b/graphics/elements/indicators/vbar.lua
index fe7f9bc..4cfb6e7 100644
--- a/graphics/elements/indicators/vbar.lua
+++ b/graphics/elements/indicators/vbar.lua
@@ -13,6 +13,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new vertical bar
---@nodiscard
@@ -99,7 +100,7 @@ local function vbar(args)
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
- return e.get()
+ return e.complete()
end
return vbar
diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua
new file mode 100644
index 0000000..87a274d
--- /dev/null
+++ b/graphics/elements/listbox.lua
@@ -0,0 +1,283 @@
+-- Scroll-able List Box Display Graphics Element
+
+local tcd = require("scada-common.tcallbackdsp")
+
+local core = require("graphics.core")
+local element = require("graphics.element")
+
+local CLICK_TYPE = core.events.CLICK_TYPE
+
+---@class listbox_args
+---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
+---@field item_pad? integer spacing (lines) between items in the list (default 0)
+---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
+---@field nav_active? cpair active colors for bar held down or arrow held down
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer 1 if omitted
+---@field width? integer parent width if omitted
+---@field height? integer parent height if omitted
+---@field gframe? graphics_frame frame instead of x/y/width/height
+---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
+
+---@class listbox_item
+---@field id string|integer element ID
+---@field e graphics_element element
+---@field y integer y position
+---@field h integer element height
+
+-- new listbox element
+---@nodiscard
+---@param args listbox_args
+---@return graphics_element element, element_id id
+local function listbox(args)
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ -- create content window for child elements
+ local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
+ e.content_window = scroll_frame
+
+ -- item list and scroll management
+ local list = {}
+ local item_pad = args.item_pad or 0
+ local scroll_offset = 0
+ local content_height = 0
+ local max_down_scroll = 0
+ -- bar control/tracking variables
+ local max_bar_height = e.frame.h - 2
+ local bar_height = 0 -- full height of bar
+ local bar_bounds = { 0, 0 } -- top and bottom of bar
+ local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
+ local holding_bar = false -- bar is being held by mouse
+ local bar_grip_pos = 0 -- where the bar was gripped by mouse down
+ local mouse_last_y = 0 -- last reported y coordinate of drag
+
+ -- draw scroll bar arrows, optionally showing one of them as pressed
+ ---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
+ local function draw_arrows(pressed_arrow)
+ local nav_fg_bg = args.nav_fg_bg or e.fg_bg
+ local active_fg_bg = args.nav_active or nav_fg_bg
+
+ -- draw up/down arrows
+ if pressed_arrow == 1 then
+ e.window.setTextColor(active_fg_bg.fgd)
+ e.window.setBackgroundColor(active_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, 1)
+ e.window.write("\x1e")
+ e.window.setTextColor(nav_fg_bg.fgd)
+ e.window.setBackgroundColor(nav_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, e.frame.h)
+ e.window.write("\x1f")
+ elseif pressed_arrow == -1 then
+ e.window.setTextColor(nav_fg_bg.fgd)
+ e.window.setBackgroundColor(nav_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, 1)
+ e.window.write("\x1e")
+ e.window.setTextColor(active_fg_bg.fgd)
+ e.window.setBackgroundColor(active_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, e.frame.h)
+ e.window.write("\x1f")
+ else
+ e.window.setTextColor(nav_fg_bg.fgd)
+ e.window.setBackgroundColor(nav_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, 1)
+ e.window.write("\x1e")
+ e.window.setCursorPos(e.frame.w, e.frame.h)
+ e.window.write("\x1f")
+ end
+
+ e.window.setTextColor(e.fg_bg.fgd)
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+
+ -- render the scroll bar and re-cacluate height & bounds
+ local function draw_bar()
+ local offset = 2 + math.abs(scroll_offset)
+
+ bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
+
+ if bar_height < 1 then
+ bar_is_scaled = true
+ -- can't do a 1:1 ratio
+ -- use minimum size bar with scaled offset
+ local scroll_progress = scroll_offset / max_down_scroll
+ offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
+ bar_height = 1
+ else
+ bar_is_scaled = false
+ end
+
+ bar_bounds = { offset, (bar_height + offset) - 1 }
+
+ for i = 2, e.frame.h - 1 do
+ if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
+ if args.nav_fg_bg ~= nil then
+ e.window.setBackgroundColor(args.nav_fg_bg.fgd)
+ else
+ e.window.setBackgroundColor(e.fg_bg.fgd)
+ end
+ else
+ if args.nav_fg_bg ~= nil then
+ e.window.setBackgroundColor(args.nav_fg_bg.bkg)
+ else
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+ end
+
+ e.window.setCursorPos(e.frame.w, i)
+ e.window.write(" ")
+ end
+
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+
+ -- update item y positions and move elements
+ local function update_positions()
+ local next_y = 1
+
+ scroll_frame.setVisible(false)
+ scroll_frame.setBackgroundColor(e.fg_bg.bkg)
+ scroll_frame.setTextColor(e.fg_bg.fgd)
+ scroll_frame.clear()
+
+ for i = 1, #list do
+ local item = list[i] ---@type listbox_item
+ item.y = next_y
+ next_y = next_y + item.h + item_pad
+ item.e.reposition(1, item.y)
+ item.e.show()
+ end
+
+ content_height = next_y
+ max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
+ if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
+
+ scroll_frame.reposition(1, 1 + scroll_offset)
+ scroll_frame.setVisible(true)
+
+ draw_bar()
+ end
+
+ -- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
+ ---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
+ local function scaled_bar_scroll(direction)
+ local scroll_progress = scroll_offset / max_down_scroll
+ local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
+
+ -- check what moving the scroll bar up or down would mean for the scroll progress
+ scroll_progress = (bar_position + direction) / (max_bar_height - 1)
+
+ return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
+ end
+
+ -- scroll down the list
+ local function scroll_down(scaled)
+ if scroll_offset > max_down_scroll then
+ if scaled then
+ scroll_offset = scaled_bar_scroll(1)
+ else
+ scroll_offset = scroll_offset - 1
+ end
+
+ update_positions()
+ end
+ end
+
+ -- scroll up the list
+ local function scroll_up(scaled)
+ if scroll_offset < 0 then
+ if scaled then
+ scroll_offset = scaled_bar_scroll(-1)
+ else
+ scroll_offset = scroll_offset + 1
+ end
+
+ update_positions()
+ end
+ end
+
+ -- handle a child element having been added to the list
+ ---@param id element_id element identifier
+ ---@param child graphics_element child element
+ function e.on_added(id, child)
+ table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
+ update_positions()
+ end
+
+ -- handle a child element having been removed from the list
+ ---@param id element_id element identifier
+ function e.on_removed(id)
+ for idx, elem in ipairs(list) do
+ if elem.id == id then
+ table.remove(list, idx)
+ update_positions()
+ return
+ end
+ end
+ end
+
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
+ if e.enabled then
+ if event.type == CLICK_TYPE.TAP then
+ if event.current.x == e.frame.w then
+ if event.current.y == 1 or event.current.y < bar_bounds[1] then
+ draw_arrows(1)
+ scroll_up()
+ if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
+ elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
+ draw_arrows(-1)
+ scroll_down()
+ if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
+ end
+ end
+ elseif event.type == CLICK_TYPE.DOWN then
+ if event.current.x == e.frame.w then
+ if event.current.y == 1 or event.current.y < bar_bounds[1] then
+ draw_arrows(1)
+ scroll_up()
+ elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
+ draw_arrows(-1)
+ scroll_down()
+ else
+ -- clicked on bar
+ holding_bar = true
+ bar_grip_pos = event.current.y - bar_bounds[1]
+ mouse_last_y = event.current.y
+ end
+ end
+ elseif event.type == CLICK_TYPE.UP then
+ holding_bar = false
+ draw_arrows(0)
+ elseif event.type == CLICK_TYPE.DRAG then
+ if holding_bar then
+ -- if mouse is within vertical frame, including the grip point
+ if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
+ if event.current.y < mouse_last_y then
+ scroll_up(bar_is_scaled)
+ elseif event.current.y > mouse_last_y then
+ scroll_down(bar_is_scaled)
+ end
+
+ mouse_last_y = event.current.y
+ end
+ end
+ elseif event.type == CLICK_TYPE.SCROLL_DOWN then
+ scroll_down()
+ elseif event.type == CLICK_TYPE.SCROLL_UP then
+ scroll_up()
+ end
+ end
+ end
+
+ draw_arrows(0)
+ draw_bar()
+
+ return e.complete()
+end
+
+return listbox
diff --git a/graphics/elements/multipane.lua b/graphics/elements/multipane.lua
index 8e25bab..790b595 100644
--- a/graphics/elements/multipane.lua
+++ b/graphics/elements/multipane.lua
@@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new multipane element
---@nodiscard
@@ -36,7 +37,7 @@ local function multipane(args)
e.set_value(1)
- return e.get()
+ return e.complete()
end
return multipane
diff --git a/graphics/elements/pipenet.lua b/graphics/elements/pipenet.lua
index 8a1d29b..5ca4745 100644
--- a/graphics/elements/pipenet.lua
+++ b/graphics/elements/pipenet.lua
@@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
+---@field hidden? boolean true to hide on initial draw
-- new pipe network
---@param args pipenet_args
@@ -141,7 +142,7 @@ local function pipenet(args)
end
- return e.get()
+ return e.complete()
end
return pipenet
diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua
index 2f7a68d..cd4b8cf 100644
--- a/graphics/elements/rectangle.lua
+++ b/graphics/elements/rectangle.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new rectangle
---@param args rectangle_args
@@ -30,27 +31,35 @@ local function rectangle(args)
end
-- offset children
+ local offset_x = 0
+ local offset_y = 0
if args.border ~= nil then
- args.offset_x = args.border.width
- args.offset_y = args.border.width
+ offset_x = args.border.width
+ offset_y = args.border.width
-- slightly different y offset if the border is set to even
if args.border.even then
local width_x2 = (2 * args.border.width)
- args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
+ offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end
end
-- create new graphics element base object
local e = element.new(args)
+ -- create content window for child elements
+ e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
+ e.content_window.setBackgroundColor(e.fg_bg.bkg)
+ e.content_window.setTextColor(e.fg_bg.fgd)
+ e.content_window.clear()
+
-- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then
e.window.setCursorPos(1, 1)
- local border_width = args.offset_x
- local border_height = args.offset_y
+ local border_width = offset_x
+ local border_height = offset_y
local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2
@@ -177,7 +186,7 @@ local function rectangle(args)
end
end
- return e.get()
+ return e.complete()
end
return rectangle
diff --git a/graphics/elements/textbox.lua b/graphics/elements/textbox.lua
index 9066deb..e72571b 100644
--- a/graphics/elements/textbox.lua
+++ b/graphics/elements/textbox.lua
@@ -18,6 +18,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new text box
---@param args textbox_args
@@ -64,7 +65,7 @@ local function textbox(args)
display_text(val)
end
- return e.get()
+ return e.complete()
end
return textbox
diff --git a/graphics/elements/tiling.lua b/graphics/elements/tiling.lua
index a97438a..536ed45 100644
--- a/graphics/elements/tiling.lua
+++ b/graphics/elements/tiling.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new tiling box
---@param args tiling_args
@@ -81,7 +82,7 @@ local function tiling(args)
if inner_width % 2 == 0 then alternator = not alternator end
end
- return e.get()
+ return e.complete()
end
return tiling
diff --git a/install_manifest.json b/install_manifest.json
new file mode 100644
index 0000000..a2f4b51
--- /dev/null
+++ b/install_manifest.json
@@ -0,0 +1 @@
+{"versions": {"installer": "v1.2", "bootloader": "0.2", "comms": "1.4.1", "reactor-plc": "v1.3.6", "rtu": "v1.2.6", "supervisor": "v0.16.6", "coordinator": "v0.15.6", "pocket": "alpha-v0.3.6"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/crypto.lua", "scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/tcallbackdsp.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua"], "graphics": ["graphics/element.lua", "graphics/events.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/listbox.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/multipane.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/tabbar.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/cipher/aes128.lua", "lockbox/cipher/aes256.lua", "lockbox/cipher/aes192.lua", "lockbox/cipher/mode/ofb.lua", "lockbox/cipher/mode/cbc.lua", "lockbox/cipher/mode/ctr.lua", "lockbox/cipher/mode/cfb.lua", "lockbox/mac/hmac.lua", "lockbox/padding/ansix923.lua", "lockbox/padding/pkcs7.lua", "lockbox/padding/zero.lua", "lockbox/padding/isoiec7816.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/renderer.lua", "rtu/threads.lua", "rtu/rtu.lua", "rtu/databus.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/panel/front_panel.lua", "rtu/panel/style.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/renderer.lua", "supervisor/databus.lua", "supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/panel/pgi.lua", "supervisor/panel/front_panel.lua", "supervisor/panel/style.lua", "supervisor/panel/components/rtu_entry.lua", "supervisor/panel/components/pdg_entry.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/pocket.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/processctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/turbine.lua", "coordinator/session/api.lua", "coordinator/session/apisessions.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/config.lua", "pocket/coreio.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/pages/turbine_page.lua", "pocket/ui/pages/reactor_page.lua", "pocket/ui/pages/home_page.lua", "pocket/ui/pages/unit_page.lua", "pocket/ui/pages/boiler_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common", "graphics"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 5775, "system": 1991, "common": 91102, "graphics": 144082, "lockbox": 100797, "reactor-plc": 95896, "rtu": 100988, "supervisor": 310851, "coordinator": 197544, "pocket": 36338}}
\ No newline at end of file
diff --git a/pocket/startup.lua b/pocket/startup.lua
index 075919d..50862d1 100644
--- a/pocket/startup.lua
+++ b/pocket/startup.lua
@@ -17,7 +17,7 @@ local coreio = require("pocket.coreio")
local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
-local POCKET_VERSION = "alpha-v0.3.2"
+local POCKET_VERSION = "alpha-v0.3.6"
local println = util.println
local println_ts = util.println_ts
@@ -120,54 +120,54 @@ local function main()
conn_wd.sv.feed()
conn_wd.api.feed()
log.debug("startup> conn watchdog started")
- end
- -- main event loop
- while ui_ok do
- local event, param1, param2, param3, param4, param5 = util.pull_event()
+ -- main event loop
+ while true do
+ local event, param1, param2, param3, param4, param5 = util.pull_event()
- -- handle event
- if event == "timer" then
- if loop_clock.is_clock(param1) then
- -- main loop tick
+ -- handle event
+ if event == "timer" then
+ if loop_clock.is_clock(param1) then
+ -- main loop tick
- -- relink if necessary
- pocket_comms.link_update()
+ -- relink if necessary
+ pocket_comms.link_update()
- loop_clock.start()
- elseif conn_wd.sv.is_timer(param1) then
- -- supervisor watchdog timeout
- log.info("supervisor server timeout")
- pocket_comms.close_sv()
- elseif conn_wd.api.is_timer(param1) then
- -- coordinator watchdog timeout
- log.info("coordinator api server timeout")
- pocket_comms.close_api()
- else
- -- a non-clock/main watchdog timer event
- -- notify timer callback dispatcher
- tcallbackdsp.handle(param1)
+ loop_clock.start()
+ elseif conn_wd.sv.is_timer(param1) then
+ -- supervisor watchdog timeout
+ log.info("supervisor server timeout")
+ pocket_comms.close_sv()
+ elseif conn_wd.api.is_timer(param1) then
+ -- coordinator watchdog timeout
+ log.info("coordinator api server timeout")
+ pocket_comms.close_api()
+ else
+ -- a non-clock/main watchdog timer event
+ -- notify timer callback dispatcher
+ tcallbackdsp.handle(param1)
+ end
+ elseif event == "modem_message" then
+ -- got a packet
+ local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
+ pocket_comms.handle_packet(packet)
+ elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
+ -- handle a monitor touch event
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
+ end
+
+ -- check for termination request
+ if event == "terminate" or ppm.should_terminate() then
+ log.info("terminate requested, closing server connections...")
+ pocket_comms.close()
+ log.info("connections closed")
+ break
end
- elseif event == "modem_message" then
- -- got a packet
- local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
- pocket_comms.handle_packet(packet)
- elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
- -- handle a monitor touch event
- renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
- -- check for termination request
- if event == "terminate" or ppm.should_terminate() then
- log.info("terminate requested, closing server connections...")
- pocket_comms.close()
- log.info("connections closed")
- break
- end
+ renderer.close_ui()
end
- renderer.close_ui()
-
println_ts("exited")
log.info("exited")
end
diff --git a/pocket/ui/components/conn_waiting.lua b/pocket/ui/components/conn_waiting.lua
index 9bbbfc0..114d165 100644
--- a/pocket/ui/components/conn_waiting.lua
+++ b/pocket/ui/components/conn_waiting.lua
@@ -25,7 +25,7 @@ local function init(parent, y, is_api)
-- bounding box div
local box = Div{parent=root,x=1,y=y,height=5}
- local waiting_x = math.floor(parent.width() / 2) - 1
+ local waiting_x = math.floor(parent.get_width() / 2) - 1
if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua
index c143b0b..11331e4 100644
--- a/pocket/ui/main.lua
+++ b/pocket/ui/main.lua
@@ -8,11 +8,11 @@ local style = require("pocket.ui.style")
local conn_waiting = require("pocket.ui.components.conn_waiting")
-local home_page = require("pocket.ui.components.home_page")
-local unit_page = require("pocket.ui.components.unit_page")
-local reactor_page = require("pocket.ui.components.reactor_page")
-local boiler_page = require("pocket.ui.components.boiler_page")
-local turbine_page = require("pocket.ui.components.turbine_page")
+local home_page = require("pocket.ui.pages.home_page")
+local unit_page = require("pocket.ui.pages.unit_page")
+local reactor_page = require("pocket.ui.pages.reactor_page")
+local boiler_page = require("pocket.ui.pages.boiler_page")
+local turbine_page = require("pocket.ui.pages.turbine_page")
local core = require("graphics.core")
diff --git a/pocket/ui/components/boiler_page.lua b/pocket/ui/pages/boiler_page.lua
similarity index 100%
rename from pocket/ui/components/boiler_page.lua
rename to pocket/ui/pages/boiler_page.lua
diff --git a/pocket/ui/components/home_page.lua b/pocket/ui/pages/home_page.lua
similarity index 100%
rename from pocket/ui/components/home_page.lua
rename to pocket/ui/pages/home_page.lua
diff --git a/pocket/ui/components/reactor_page.lua b/pocket/ui/pages/reactor_page.lua
similarity index 100%
rename from pocket/ui/components/reactor_page.lua
rename to pocket/ui/pages/reactor_page.lua
diff --git a/pocket/ui/components/turbine_page.lua b/pocket/ui/pages/turbine_page.lua
similarity index 100%
rename from pocket/ui/components/turbine_page.lua
rename to pocket/ui/pages/turbine_page.lua
diff --git a/pocket/ui/components/unit_page.lua b/pocket/ui/pages/unit_page.lua
similarity index 100%
rename from pocket/ui/components/unit_page.lua
rename to pocket/ui/pages/unit_page.lua
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index 1a7f550..3e8861b 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
-local R_PLC_VERSION = "v1.3.2"
+local R_PLC_VERSION = "v1.3.6"
local println = util.println
local println_ts = util.println_ts
diff --git a/rtu/startup.lua b/rtu/startup.lua
index 6ff5e7d..86c3db2 100644
--- a/rtu/startup.lua
+++ b/rtu/startup.lua
@@ -28,7 +28,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
-local RTU_VERSION = "v1.2.2"
+local RTU_VERSION = "v1.2.6"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@@ -457,9 +457,9 @@ local function main()
if not rtu_state.fp_ok then
renderer.close_ui()
println_ts(util.c("UI error: ", message))
- println("init> running without front panel")
+ println("startup> running without front panel")
log.error(util.c("GUI crashed with error ", message))
- log.info("init> running in headless mode without front panel")
+ log.info("startup> running in headless mode without front panel")
end
-- start connection watchdog
diff --git a/supervisor/databus.lua b/supervisor/databus.lua
new file mode 100644
index 0000000..a1e947e
--- /dev/null
+++ b/supervisor/databus.lua
@@ -0,0 +1,174 @@
+--
+-- Data Bus - Central Communication Linking for Supervisor Front Panel
+--
+
+local psil = require("scada-common.psil")
+
+local pgi = require("supervisor.panel.pgi")
+
+local databus = {}
+
+-- databus PSIL
+databus.ps = psil.create()
+
+-- call to toggle heartbeat signal
+function databus.heartbeat() databus.ps.toggle("heartbeat") end
+
+-- transmit firmware versions across the bus
+---@param sv_v string supervisor version
+---@param comms_v string comms version
+function databus.tx_versions(sv_v, comms_v)
+ databus.ps.publish("version", sv_v)
+ databus.ps.publish("comms_version", comms_v)
+end
+
+-- transmit hardware status for modem connection state
+---@param has_modem boolean
+function databus.tx_hw_modem(has_modem)
+ databus.ps.publish("has_modem", has_modem)
+end
+
+-- transmit PLC firmware version and session connection state
+---@param reactor_id integer reactor unit ID
+---@param fw string firmware version
+---@param channel integer PLC remote port
+function databus.tx_plc_connected(reactor_id, fw, channel)
+ databus.ps.publish("plc_" .. reactor_id .. "_fw", fw)
+ databus.ps.publish("plc_" .. reactor_id .. "_conn", true)
+ databus.ps.publish("plc_" .. reactor_id .. "_chan", tostring(channel))
+end
+
+-- transmit PLC disconnected
+---@param reactor_id integer reactor unit ID
+function databus.tx_plc_disconnected(reactor_id)
+ databus.ps.publish("plc_" .. reactor_id .. "_fw", " ------- ")
+ databus.ps.publish("plc_" .. reactor_id .. "_conn", false)
+ databus.ps.publish("plc_" .. reactor_id .. "_chan", " --- ")
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt", 0)
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.lightGray)
+end
+
+-- transmit PLC session RTT
+---@param reactor_id integer reactor unit ID
+---@param rtt integer round trip time
+function databus.tx_plc_rtt(reactor_id, rtt)
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green)
+ end
+end
+
+-- transmit RTU firmware version and session connection state
+---@param session_id integer RTU session
+---@param fw string firmware version
+---@param channel integer RTU remote port
+function databus.tx_rtu_connected(session_id, fw, channel)
+ databus.ps.publish("rtu_" .. session_id .. "_fw", fw)
+ databus.ps.publish("rtu_" .. session_id .. "_chan", tostring(channel))
+ pgi.create_rtu_entry(session_id)
+end
+
+-- transmit RTU disconnected
+---@param session_id integer RTU session
+function databus.tx_rtu_disconnected(session_id)
+ pgi.delete_rtu_entry(session_id)
+end
+
+-- transmit RTU session RTT
+---@param session_id integer RTU session
+---@param rtt integer round trip time
+function databus.tx_rtu_rtt(session_id, rtt)
+ databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green)
+ end
+end
+
+-- transmit RTU session unit count
+---@param session_id integer RTU session
+---@param units integer unit count
+function databus.tx_rtu_units(session_id, units)
+ databus.ps.publish("rtu_" .. session_id .. "_units", units)
+end
+
+-- transmit coordinator firmware version and session connection state
+---@param fw string firmware version
+---@param channel integer coordinator remote port
+function databus.tx_crd_connected(fw, channel)
+ databus.ps.publish("crd_fw", fw)
+ databus.ps.publish("crd_conn", true)
+ databus.ps.publish("crd_chan", tostring(channel))
+end
+
+-- transmit coordinator disconnected
+function databus.tx_crd_disconnected()
+ databus.ps.publish("crd_fw", " ------- ")
+ databus.ps.publish("crd_conn", false)
+ databus.ps.publish("crd_chan", "---")
+ databus.ps.publish("crd_rtt", 0)
+ databus.ps.publish("crd_rtt_color", colors.lightGray)
+end
+
+-- transmit coordinator session RTT
+---@param rtt integer round trip time
+function databus.tx_crd_rtt(rtt)
+ databus.ps.publish("crd_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("crd_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("crd_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("crd_rtt_color", colors.green)
+ end
+end
+
+-- transmit PKT firmware version and PDG session connection state
+---@param session_id integer PDG session
+---@param fw string firmware version
+---@param channel integer PDG remote port
+function databus.tx_pdg_connected(session_id, fw, channel)
+ databus.ps.publish("pdg_" .. session_id .. "_fw", fw)
+ databus.ps.publish("pdg_" .. session_id .. "_chan", tostring(channel))
+ pgi.create_pdg_entry(session_id)
+end
+
+-- transmit PDG session disconnected
+---@param session_id integer PDG session
+function databus.tx_pdg_disconnected(session_id)
+ pgi.delete_pdg_entry(session_id)
+end
+
+-- transmit PDG session RTT
+---@param session_id integer PDG session
+---@param rtt integer round trip time
+function databus.tx_pdg_rtt(session_id, rtt)
+ databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green)
+ end
+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)
+ databus.ps.subscribe(field, func)
+end
+
+return databus
diff --git a/supervisor/panel/components/pdg_entry.lua b/supervisor/panel/components/pdg_entry.lua
new file mode 100644
index 0000000..94cd385
--- /dev/null
+++ b/supervisor/panel/components/pdg_entry.lua
@@ -0,0 +1,48 @@
+--
+-- Pocket Diagnostics Connection Entry
+--
+
+local util = require("scada-common.util")
+
+local databus = require("supervisor.databus")
+
+local core = require("graphics.core")
+
+local Div = require("graphics.elements.div")
+local TextBox = require("graphics.elements.textbox")
+
+local DataIndicator = require("graphics.elements.indicators.data")
+
+local TEXT_ALIGN = core.TEXT_ALIGN
+
+local cpair = core.cpair
+
+-- create a pocket diagnostics list entry
+---@param parent graphics_element parent
+---@param id integer PDG session ID
+local function init(parent, id)
+ -- root div
+ local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
+ local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
+
+ local ps_prefix = "pdg_" .. id .. "_"
+
+ TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ local pdg_chan = TextBox{parent=entry,x=1,y=2,text=" :00000",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
+ TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ pdg_chan.register(databus.ps, ps_prefix .. "chan", function (channel) pdg_chan.set_value(util.sprintf(" :%05d", channel)) end)
+
+ TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
+ local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
+
+ TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
+ local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
+ pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)
+
+ return root
+end
+
+return init
diff --git a/supervisor/panel/components/rtu_entry.lua b/supervisor/panel/components/rtu_entry.lua
new file mode 100644
index 0000000..a4a5a4e
--- /dev/null
+++ b/supervisor/panel/components/rtu_entry.lua
@@ -0,0 +1,52 @@
+--
+-- RTU Connection Entry
+--
+
+local util = require("scada-common.util")
+
+local databus = require("supervisor.databus")
+
+local core = require("graphics.core")
+
+local Div = require("graphics.elements.div")
+local TextBox = require("graphics.elements.textbox")
+
+local DataIndicator = require("graphics.elements.indicators.data")
+
+local TEXT_ALIGN = core.TEXT_ALIGN
+
+local cpair = core.cpair
+
+-- create an RTU list entry
+---@param parent graphics_element parent
+---@param id integer RTU session ID
+local function init(parent, id)
+ -- root div
+ local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
+ local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
+
+ local ps_prefix = "rtu_" .. id .. "_"
+
+ TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ local rtu_chan = TextBox{parent=entry,x=1,y=2,text=" :00000",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
+ TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ rtu_chan.register(databus.ps, ps_prefix .. "chan", function (channel) rtu_chan.set_value(util.sprintf(" :%05d", channel)) end)
+
+ TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1}
+ local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=cpair(colors.gray,colors.white)}
+ unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
+
+ TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1}
+ local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
+
+ TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1}
+ local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
+ rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)
+
+ return root
+end
+
+return init
diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua
new file mode 100644
index 0000000..8cdcf76
--- /dev/null
+++ b/supervisor/panel/front_panel.lua
@@ -0,0 +1,160 @@
+--
+-- Main SCADA Coordinator GUI
+--
+
+local util = require("scada-common.util")
+
+local config = require("supervisor.config")
+local databus = require("supervisor.databus")
+
+local pgi = require("supervisor.panel.pgi")
+local style = require("supervisor.panel.style")
+
+local pdg_entry = require("supervisor.panel.components.pdg_entry")
+local rtu_entry = require("supervisor.panel.components.rtu_entry")
+
+local core = require("graphics.core")
+
+local Div = require("graphics.elements.div")
+local ListBox = require("graphics.elements.listbox")
+local MultiPane = require("graphics.elements.multipane")
+local TextBox = require("graphics.elements.textbox")
+
+local TabBar = require("graphics.elements.controls.tabbar")
+
+local LED = require("graphics.elements.indicators.led")
+local DataIndicator = require("graphics.elements.indicators.data")
+
+local TEXT_ALIGN = core.TEXT_ALIGN
+
+local cpair = core.cpair
+
+-- create new main view
+---@param panel graphics_element main displaybox
+local function init(panel)
+ TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
+
+ local page_div = Div{parent=panel,x=1,y=3}
+
+ --
+ -- system indicators
+ --
+
+ local main_page = Div{parent=page_div,x=1,y=1}
+
+ local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
+
+ local on = 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)}
+ on.update(true)
+ system.line_break()
+
+ heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
+
+ local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
+ system.line_break()
+
+ modem.register(databus.ps, "has_modem", modem.update)
+
+ --
+ -- about footer
+ --
+
+ local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
+ local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
+ local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
+
+ fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
+ comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
+
+ --
+ -- page handling
+ --
+
+ -- plc page
+
+ local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
+
+ for i = 1, config.NUM_REACTORS do
+ local ps_prefix = "plc_" .. i .. "_"
+ local plc_entry = Div{parent=plc_list,height=3,fg_bg=cpair(colors.black,colors.white)}
+
+ TextBox{parent=plc_entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+
+ local conn = LED{parent=plc_entry,x=10,y=2,label="CONN",colors=cpair(colors.green,colors.green_off)}
+ conn.register(databus.ps, ps_prefix .. "conn", conn.update)
+
+ local plc_chan = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)}
+ plc_chan.register(databus.ps, ps_prefix .. "chan", plc_chan.set_value)
+
+ TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1}
+ local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
+
+ TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1}
+ local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
+ plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
+
+ plc_list.line_break()
+ end
+
+ -- rtu page
+
+ local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
+ local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding
+
+ -- coordinator page
+
+ local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=cpair(colors.black,colors.white)}
+
+ local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)}
+ crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
+
+ TextBox{parent=crd_box,x=4,y=3,text="CHANNEL ",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)}
+ local crd_chan = TextBox{parent=crd_box,x=12,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)}
+ crd_chan.register(databus.ps, "crd_chan", crd_chan.set_value)
+
+ TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1}
+ local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
+
+ TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1}
+ local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
+ crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
+
+ -- pocket diagnostics page
+
+ local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
+ local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding
+
+ -- assemble page panes
+
+ local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page }
+
+ local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
+
+ local tabs = {
+ { name = "SVR", color = cpair(colors.black, colors.ivory) },
+ { name = "PLC", color = cpair(colors.black, colors.ivory) },
+ { name = "RTU", color = cpair(colors.black, colors.ivory) },
+ { name = "CRD", color = cpair(colors.black, colors.ivory) },
+ { name = "PKT", color = cpair(colors.black, colors.ivory) },
+ }
+
+ TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
+
+ -- link RTU/PDG list management to PGI
+ pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
+end
+
+return init
diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua
new file mode 100644
index 0000000..9065f72
--- /dev/null
+++ b/supervisor/panel/pgi.lua
@@ -0,0 +1,93 @@
+--
+-- Protected Graphics Interface
+--
+
+local log = require("scada-common.log")
+local util = require("scada-common.util")
+
+local pgi = {}
+
+local data = {
+ rtu_list = nil, ---@type nil|graphics_element
+ pdg_list = nil, ---@type nil|graphics_element
+ rtu_entry = nil, ---@type function
+ pdg_entry = nil, ---@type function
+ -- session entries
+ s_entries = { rtu = {}, pdg = {} }
+}
+
+-- link list boxes
+---@param rtu_list graphics_element RTU list element
+---@param rtu_entry function RTU entry constructor
+---@param pdg_list graphics_element pocket diagnostics list element
+---@param pdg_entry function pocket diagnostics entry constructor
+function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
+ data.rtu_list = rtu_list
+ data.pdg_list = pdg_list
+ data.rtu_entry = rtu_entry
+ data.pdg_entry = pdg_entry
+end
+
+-- unlink all fields, disabling the PGI
+function pgi.unlink()
+ data.rtu_list = nil
+ data.pdg_list = nil
+ data.rtu_entry = nil
+ data.pdg_entry = nil
+end
+
+-- add an RTU entry to the RTU list
+---@param session_id integer RTU session
+function pgi.create_rtu_entry(session_id)
+ if data.rtu_list ~= nil and data.rtu_entry ~= nil then
+ local success, result = pcall(data.rtu_entry, data.rtu_list, session_id)
+
+ if success then
+ data.s_entries.rtu[session_id] = result
+ else
+ log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
+ end
+ end
+end
+
+-- delete an RTU entry from the RTU list
+---@param session_id integer RTU session
+function pgi.delete_rtu_entry(session_id)
+ if data.s_entries.rtu[session_id] ~= nil then
+ local success, result = pcall(data.s_entries.rtu[session_id].delete)
+ data.s_entries.rtu[session_id] = nil
+
+ if not success then
+ log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
+ end
+ end
+end
+
+-- add a PDG entry to the PDG list
+---@param session_id integer pocket diagnostics session
+function pgi.create_pdg_entry(session_id)
+ if data.pdg_list ~= nil and data.pdg_entry ~= nil then
+ local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
+
+ if success then
+ data.s_entries.pdg[session_id] = result
+ else
+ log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
+ end
+ end
+end
+
+-- delete a PDG entry from the PDG list
+---@param session_id integer pocket diagnostics session
+function pgi.delete_pdg_entry(session_id)
+ if data.s_entries.pdg[session_id] ~= nil then
+ local success, result = pcall(data.s_entries.pdg[session_id].delete)
+ data.s_entries.pdg[session_id] = nil
+
+ if not success then
+ log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
+ end
+ end
+end
+
+return pgi
diff --git a/supervisor/panel/style.lua b/supervisor/panel/style.lua
new file mode 100644
index 0000000..0668ccc
--- /dev/null
+++ b/supervisor/panel/style.lua
@@ -0,0 +1,42 @@
+--
+-- Graphics Style Options
+--
+
+local core = require("graphics.core")
+
+local style = {}
+
+local cpair = core.cpair
+
+-- GLOBAL --
+
+-- remap global colors
+colors.ivory = colors.pink
+colors.yellow_hc = colors.purple
+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 }, -- YELLOW ON
+ { 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 = 0x0008fe }, -- LCD BLUE
+ { c = colors.purple, hex = 0xe3bc2a }, -- YELLOW HIGH CONTRAST
+ { 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/supervisor/renderer.lua b/supervisor/renderer.lua
new file mode 100644
index 0000000..1bc70a4
--- /dev/null
+++ b/supervisor/renderer.lua
@@ -0,0 +1,84 @@
+--
+-- Graphics Rendering Control
+--
+
+local panel_view = require("supervisor.panel.front_panel")
+local pgi = require("supervisor.panel.pgi")
+local style = require("supervisor.panel.style")
+
+local flasher = require("graphics.flasher")
+
+local DisplayBox = require("graphics.elements.displaybox")
+
+local renderer = {}
+
+local ui = {
+ display = nil
+}
+
+-- start the UI
+function renderer.start_ui()
+ if ui.display == 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
+
+ -- init front panel view
+ ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
+ panel_view(ui.display)
+
+ -- start flasher callback task
+ flasher.run()
+ end
+end
+
+-- close out the UI
+function renderer.close_ui()
+ if ui.display ~= nil then
+ -- stop blinking indicators
+ flasher.clear()
+
+ -- disable PGI
+ pgi.unlink()
+
+ -- hide to stop animation callbacks
+ ui.display.hide()
+
+ -- clear root UI elements
+ ui.display = 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
+end
+
+-- is the UI ready?
+---@nodiscard
+---@return boolean ready
+function renderer.ui_ready() return ui.display ~= nil end
+
+-- handle a mouse event
+---@param event mouse_interaction|nil
+function renderer.handle_mouse(event)
+ if ui.display ~= nil and event ~= nil then
+ ui.display.handle_mouse(event)
+ end
+end
+
+return renderer
diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua
index b093d87..36a5241 100644
--- a/supervisor/session/coordinator.lua
+++ b/supervisor/session/coordinator.lua
@@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local databus = require("supervisor.databus")
+
local svqtypes = require("supervisor.session.svqtypes")
local coordinator = {}
@@ -18,8 +20,6 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local SV_Q_DATA = svqtypes.SV_Q_DATA
-local println = util.println
-
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
@@ -49,7 +49,11 @@ local PERIODICS = {
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param facility facility facility data table
-function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
+---@param fp_ok boolean if the front panel UI is running
+function coordinator.new_session(id, in_queue, out_queue, timeout, facility, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local log_header = "crdn_session(" .. id .. "): "
local self = {
@@ -84,6 +88,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_crd_disconnected()
end
-- send a CRDN packet
@@ -205,6 +210,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "COORD TT = " .. (srv_now - coord_send) .. "ms")
+
+ databus.tx_crd_rtt(self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua
index cca8a26..1436534 100644
--- a/supervisor/session/plc.lua
+++ b/supervisor/session/plc.lua
@@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local databus = require("supervisor.databus")
+
local svqtypes = require("supervisor.session.svqtypes")
local plc = {}
@@ -14,8 +16,6 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
local UNIT_COMMAND = comms.UNIT_COMMAND
-local println = util.println
-
-- retry time constants in ms
local INITIAL_WAIT = 1500
local INITIAL_AUTO_WAIT = 1000
@@ -49,7 +49,11 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
-function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
+---@param fp_ok boolean if the front panel UI is running
+function plc.new_session(id, reactor_id, in_queue, out_queue, timeout, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local log_header = "plc_session(" .. id .. "): "
local self = {
@@ -235,6 +239,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_plc_disconnected(reactor_id)
end
-- send an RPLC packet
@@ -485,6 +490,8 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = " .. (srv_now - plc_send) .. "ms")
+
+ databus.tx_plc_rtt(reactor_id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua
index 9cf0a5d..ba6d179 100644
--- a/supervisor/session/pocket.lua
+++ b/supervisor/session/pocket.lua
@@ -1,15 +1,14 @@
-local comms = require("scada-common.comms")
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local util = require("scada-common.util")
+local comms = require("scada-common.comms")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local util = require("scada-common.util")
+local databus = require("supervisor.databus")
local pocket = {}
local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
-local println = util.println
-
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000
@@ -33,8 +32,12 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
-function pocket.new_session(id, in_queue, out_queue, timeout)
- local log_header = "diag_session(" .. id .. "): "
+---@param fp_ok boolean if the front panel UI is running
+function pocket.new_session(id, in_queue, out_queue, timeout, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
+ local log_header = "pdg_session(" .. id .. "): "
local self = {
-- connection properties
@@ -55,18 +58,19 @@ function pocket.new_session(id, in_queue, out_queue, timeout)
acks = {
},
-- session database
- ---@class diag_db
+ ---@class pdg_db
sDB = {
}
}
- ---@class diag_session
+ ---@class pdg_session
local public = {}
-- mark this diagnostics session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_pdg_disconnected(id)
end
-- send a SCADA management packet
@@ -106,16 +110,18 @@ function pocket.new_session(id, in_queue, out_queue, timeout)
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
- -- local diag_send = pkt.data[2]
+ -- local pdg_send = pkt.data[2]
local srv_now = util.time()
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
- log.warning(log_header .. "DIAG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
+ log.warning(log_header .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
- -- log.debug(log_header .. "DIAG RTT = " .. self.last_rtt .. "ms")
- -- log.debug(log_header .. "DIAG TT = " .. (srv_now - diag_send) .. "ms")
+ -- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms")
+ -- log.debug(log_header .. "PDG TT = " .. (srv_now - pdg_send) .. "ms")
+
+ databus.tx_pdg_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua
index b494aa6..2f7091c 100644
--- a/supervisor/session/rtu.lua
+++ b/supervisor/session/rtu.lua
@@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local databus = require("supervisor.databus")
+
local svqtypes = require("supervisor.session.svqtypes")
-- supervisor rtu sessions (svrs)
@@ -22,8 +24,6 @@ local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
-local println = util.println
-
local PERIODICS = {
KEEP_ALIVE = 2000
}
@@ -36,7 +36,11 @@ local PERIODICS = {
---@param timeout number communications timeout
---@param advertisement table RTU device advertisement
---@param facility facility facility data table
-function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facility)
+---@param fp_ok boolean if the front panel UI is running
+function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local log_header = "rtu_session(" .. id .. "): "
local self = {
@@ -66,6 +70,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- parse the recorded advertisement and create unit sub-sessions
local function _handle_advertisement()
+ local unit_count = 0
+
_reset_config()
for i = 1, #self.fac_units do
@@ -172,18 +178,22 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
if unit ~= nil then
self.units[i] = unit
+ unit_count = unit_count + 1
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
_reset_config()
log.error(util.c(log_header, "bad advertisement: error occured while creating a unit (type is ", type_string, ")"))
break
end
end
+
+ databus.tx_rtu_units(id, unit_count)
end
-- mark this RTU session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_rtu_disconnected(id)
-- mark all RTU unit sessions as closed so the reactor unit knows
for _, unit in pairs(self.units) do unit.close() end
@@ -254,6 +264,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
+
+ databus.tx_rtu_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua
index 8ad3366..5d22e3d 100644
--- a/supervisor/session/svsessions.lua
+++ b/supervisor/session/svsessions.lua
@@ -3,6 +3,7 @@ local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local config = require("supervisor.config")
+local databus = require("supervisor.databus")
local facility = require("supervisor.facility")
local svqtypes = require("supervisor.session.svqtypes")
@@ -22,24 +23,26 @@ local CRD_S_DATA = coordinator.CRD_S_DATA
local svsessions = {}
+---@enum SESSION_TYPE
local SESSION_TYPE = {
RTU_SESSION = 0, -- RTU gateway
PLC_SESSION = 1, -- reactor PLC
COORD_SESSION = 2, -- coordinator
- DIAG_SESSION = 3 -- pocket diagnostics
+ PDG_SESSION = 3 -- pocket diagnostics
}
svsessions.SESSION_TYPE = SESSION_TYPE
local self = {
modem = nil, ---@type table|nil
+ fp_ok = false,
num_reactors = 0,
facility = nil, ---@type facility|nil
- sessions = { rtu = {}, plc = {}, coord = {}, diag = {} },
- next_ids = { rtu = 0, plc = 0, coord = 0, diag = 0 }
+ sessions = { rtu = {}, plc = {}, coord = {}, pdg = {} },
+ next_ids = { rtu = 0, plc = 0, coord = 0, pdg = 0 }
}
----@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|diag_session_struct
+---@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|pdg_session_struct
-- PRIVATE FUNCTIONS --
@@ -194,11 +197,13 @@ end
-- PUBLIC FUNCTIONS --
-- initialize svsessions
----@param modem table
----@param num_reactors integer
----@param cooling_conf table
-function svsessions.init(modem, num_reactors, cooling_conf)
+---@param modem table modem device
+---@param fp_ok boolean front panel active
+---@param num_reactors integer number of reactors
+---@param cooling_conf table cooling configuration definition
+function svsessions.init(modem, fp_ok, num_reactors, cooling_conf)
self.modem = modem
+ self.fp_ok = fp_ok
self.num_reactors = num_reactors
self.facility = facility.new(num_reactors, cooling_conf)
end
@@ -249,14 +254,14 @@ end
-- find a coordinator or diagnostic access session by the remote port
---@nodiscard
---@param remote_port integer
----@return coord_session_struct|diag_session_struct|nil
+---@return coord_session_struct|pdg_session_struct|nil
function svsessions.find_svctl_session(remote_port)
-- check coordinator sessions
local session = _find_session(self.sessions.coord, remote_port)
-- check diagnostic sessions
- if session == nil then session = _find_session(self.sessions.diag, remote_port) end
- ---@cast session coord_session_struct|diag_session_struct|nil
+ if session == nil then session = _find_session(self.sessions.pdg, remote_port) end
+ ---@cast session coord_session_struct|pdg_session_struct|nil
return session
end
@@ -306,13 +311,17 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
instance = nil ---@type plc_session
}
- plc_s.instance = plc.new_session(self.next_ids.plc, for_reactor, plc_s.in_queue, plc_s.out_queue, config.PLC_TIMEOUT)
+ plc_s.instance = plc.new_session(self.next_ids.plc, for_reactor, plc_s.in_queue, plc_s.out_queue,
+ config.PLC_TIMEOUT, self.fp_ok)
table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units()
units[for_reactor].link_plc_session(plc_s)
- log.debug(util.c("established new PLC session to ", remote_port, " with ID ", self.next_ids.plc, " for reactor ", for_reactor))
+ log.debug(util.c("established new PLC session to ", remote_port, " with ID ", self.next_ids.plc,
+ " for reactor ", for_reactor))
+
+ databus.tx_plc_connected(for_reactor, version, remote_port)
self.next_ids.plc = self.next_ids.plc + 1
@@ -344,11 +353,14 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement
instance = nil ---@type rtu_session
}
- rtu_s.instance = rtu.new_session(self.next_ids.rtu, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement, self.facility)
+ rtu_s.instance = rtu.new_session(self.next_ids.rtu, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement,
+ self.facility, self.fp_ok)
table.insert(self.sessions.rtu, rtu_s)
log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_ids.rtu)
+ databus.tx_rtu_connected(self.next_ids.rtu, version, remote_port)
+
self.next_ids.rtu = self.next_ids.rtu + 1
-- success
@@ -375,11 +387,14 @@ function svsessions.establish_coord_session(local_port, remote_port, version)
instance = nil ---@type coord_session
}
- coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT, self.facility)
+ coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT,
+ self.facility, self.fp_ok)
table.insert(self.sessions.coord, coord_s)
log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_ids.coord)
+ databus.tx_crd_connected(version, remote_port)
+
self.next_ids.coord = self.next_ids.coord + 1
-- success
@@ -396,9 +411,9 @@ end
---@param remote_port integer
---@param version string
---@return integer|false session_id
-function svsessions.establish_diag_session(local_port, remote_port, version)
- ---@class diag_session_struct
- local diag_s = {
+function svsessions.establish_pdg_session(local_port, remote_port, version)
+ ---@class pdg_session_struct
+ local pdg_s = {
s_type = "pkt",
open = true,
version = version,
@@ -406,18 +421,20 @@ function svsessions.establish_diag_session(local_port, remote_port, version)
r_port = remote_port,
in_queue = mqueue.new(),
out_queue = mqueue.new(),
- instance = nil ---@type diag_session
+ instance = nil ---@type pdg_session
}
- diag_s.instance = pocket.new_session(self.next_ids.diag, diag_s.in_queue, diag_s.out_queue, config.PKT_TIMEOUT)
- table.insert(self.sessions.diag, diag_s)
+ pdg_s.instance = pocket.new_session(self.next_ids.pdg, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok)
+ table.insert(self.sessions.pdg, pdg_s)
- log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.diag)
+ log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.pdg)
- self.next_ids.diag = self.next_ids.diag + 1
+ databus.tx_pdg_connected(self.next_ids.pdg, version, remote_port)
+
+ self.next_ids.pdg = self.next_ids.pdg + 1
-- success
- return diag_s.instance.get_id()
+ return pdg_s.instance.get_id()
end
-- attempt to identify which session's watchdog timer fired
diff --git a/supervisor/startup.lua b/supervisor/startup.lua
index 534a5d9..06853ef 100644
--- a/supervisor/startup.lua
+++ b/supervisor/startup.lua
@@ -5,16 +5,22 @@
require("/initenv").init_env()
local crash = require("scada-common.crash")
+local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcallbackdsp")
local util = require("scada-common.util")
+local core = require("graphics.core")
+
local config = require("supervisor.config")
+local databus = require("supervisor.databus")
+local renderer = require("supervisor.renderer")
local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
-local SUPERVISOR_VERSION = "v0.15.9"
+local SUPERVISOR_VERSION = "v0.16.6"
local println = util.println
local println_ts = util.println_ts
@@ -79,6 +85,9 @@ local function main()
-- startup
----------------------------------------
+ -- record firmware versions and ID
+ databus.tx_versions(SUPERVISOR_VERSION, comms.version)
+
-- mount connected devices
ppm.mount_all()
@@ -89,9 +98,23 @@ local function main()
return
end
+ databus.tx_hw_modem(true)
+
+ -- start UI
+ local fp_ok, message = pcall(renderer.start_ui)
+
+ if not fp_ok then
+ renderer.close_ui()
+ println_ts(util.c("UI error: ", message))
+ log.error(util.c("GUI crashed with error ", message))
+ else
+ -- redefine println_ts local to not print as we have the front panel running
+ println_ts = function (_) end
+ end
+
-- start comms, open all channels
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem,
- config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE)
+ config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE, fp_ok)
-- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15
@@ -100,6 +123,9 @@ local function main()
-- start clock
loop_clock.start()
+ -- halve the rate heartbeat LED flash
+ local heartbeat_toggle = true
+
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -114,6 +140,7 @@ local function main()
if device == modem then
println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected")
+ databus.tx_hw_modem(false)
else
log.warning("non-comms modem disconnected")
end
@@ -131,6 +158,8 @@ local function main()
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
+
+ databus.tx_hw_modem(true)
else
log.info("wired modem reconnected")
end
@@ -139,6 +168,9 @@ local function main()
elseif event == "timer" and loop_clock.is_clock(param1) then
-- main loop tick
+ if heartbeat_toggle then databus.heartbeat() end
+ heartbeat_toggle = not heartbeat_toggle
+
-- iterate sessions
svsessions.iterate_all()
@@ -149,10 +181,16 @@ local function main()
elseif event == "timer" then
-- a non-clock timer event, check watchdogs
svsessions.check_all_watchdogs(param1)
+
+ -- notify timer callback dispatcher
+ tcd.handle(param1)
elseif event == "modem_message" then
-- got a packet
local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5)
superv_comms.handle_packet(packet)
+ elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
+ -- handle a mouse event
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
@@ -165,8 +203,15 @@ local function main()
end
end
- println_ts("exited")
+ renderer.close_ui()
+
+ util.println_ts("exited")
log.info("exited")
end
-if not xpcall(main, crash.handler) then crash.exit() else log.close() end
+if not xpcall(main, crash.handler) then
+ pcall(renderer.close_ui)
+ crash.exit()
+else
+ log.close()
+end
diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua
index 998c438..874aaa9 100644
--- a/supervisor/supervisor.lua
+++ b/supervisor/supervisor.lua
@@ -11,8 +11,6 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
-local println = util.println
-
-- supervisory controller communications
---@nodiscard
---@param _version string supervisor version
@@ -22,8 +20,12 @@ local println = util.println
---@param dev_listen integer listening port for PLC/RTU devices
---@param svctl_listen integer listening port for supervisor access
---@param range integer trusted device connection range
+---@param fp_ok boolean if the front panel UI is running
---@diagnostic disable-next-line: unused-local
-function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_listen, svctl_listen, range)
+function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_listen, svctl_listen, range, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local self = {
last_est_acks = {}
}
@@ -41,8 +43,8 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
_conf_channels()
- -- link modem to svsessions
- svsessions.init(modem, num_reactors, cooling_conf)
+ -- pass modem, status, and config data to svsessions
+ svsessions.init(modem, fp_ok, num_reactors, cooling_conf)
-- send an establish request response to a PLC/RTU
---@param dest integer
@@ -304,7 +306,7 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste
end
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
- local s_id = svsessions.establish_diag_session(l_port, r_port, firmware_v)
+ local s_id = svsessions.establish_pdg_session(l_port, r_port, firmware_v)
println(util.c("PKT (", firmware_v, ") [:", r_port, "] \xbb connected"))
log.info(util.c("SVCTL_ESTABLISH: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id))