diff --git a/graphics/element.lua b/graphics/element.lua
index edcbdcc..891257d 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -3,6 +3,7 @@
--
local core = require("graphics.core")
+local log = require("scada-common.log")
local element = {}
@@ -46,12 +47,18 @@ local element = {}
---|colormap_args
---|displaybox_args
---|div_args
+---|listbox_args
---|multipane_args
---|pipenet_args
---|rectangle_args
---|textbox_args
---|tiling_args
+---@class element_subscription
+---@field ps psil ps used
+---@field key string data key
+---@field func function callback
+
-- a base graphics element, should not be created on its own
---@nodiscard
---@param args graphics_args arguments
@@ -66,6 +73,7 @@ function element.new(args)
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
next_y = 1,
children = {},
+ subscriptions = {},
mt = {}
}
@@ -183,6 +191,17 @@ 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)
+ end
+
+ -- dynamically remove a child element
+ ---@param id string|integer element identifier
+ function protected.remove(id)
+ end
+
-- handle a mouse event
---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event)
@@ -281,7 +300,25 @@ function element.new(args)
---@nodiscard
function public.window() return protected.window end
- -- CHILD ELEMENTS --
+ -- delete this element (hide and unsubscribe from PSIL)
+ function public.delete()
+ -- hide + stop animations
+ public.hide()
+
+ -- unsubscribe from PSIL
+ for i = 1, #self.subscriptions do
+ local s = self.subscriptions[i] ---@type element_subscription
+ s.ps.unsubscribe(s.key, s.func)
+ end
+
+ -- delete all children
+ for k, v in pairs(self.children) do
+ v.delete()
+ self.children[k] = nil
+ end
+ end
+
+ -- ELEMENT TREE --
-- add a child element
---@nodiscard
@@ -311,12 +348,18 @@ function element.new(args)
-- get a child element
---@nodiscard
+ ---@param id element_id
---@return graphics_element
- function public.get_child(key) return self.children[key] end
+ function public.get_child(id) return self.children[id] end
- -- remove child
- ---@param key string|integer
- function public.remove(key) self.children[key] = nil 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
+ end
+ end
-- attempt to get a child element by ID (does not include this element itself)
---@nodiscard
@@ -335,6 +378,25 @@ function element.new(args)
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
@@ -460,6 +522,16 @@ function element.new(args)
protected.response_callback(result)
end
+ -- register a callback with a PSIL, allowing for automatic unregister on delete
+ -- do not use graphics elements directly with PSIL subscribe()
+ ---@param ps psil PSIL to subscribe to
+ ---@param key string key to subscribe to
+ ---@param func function function to link
+ function public.register(ps, key, func)
+ table.insert(self.subscriptions, { ps = ps, key = key, func = func })
+ ps.subscribe(key, func)
+ end
+
-- VISIBILITY --
-- show the element
diff --git a/rtu/databus.lua b/rtu/databus.lua
index 8ef720e..3014367 100644
--- a/rtu/databus.lua
+++ b/rtu/databus.lua
@@ -7,9 +7,8 @@ local util = require("scada-common.util")
local databus = {}
-local dbus_iface = {
- ps = psil.create()
-}
+-- databus PSIL
+databus.ps = psil.create()
---@enum RTU_UNIT_HW_STATE
local RTU_UNIT_HW_STATE = {
@@ -22,54 +21,54 @@ local RTU_UNIT_HW_STATE = {
databus.RTU_UNIT_HW_STATE = RTU_UNIT_HW_STATE
-- call to toggle heartbeat signal
-function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
+function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
---@param rtu_v string RTU version
---@param comms_v string comms version
function databus.tx_versions(rtu_v, comms_v)
- dbus_iface.ps.publish("version", rtu_v)
- dbus_iface.ps.publish("comms_version", comms_v)
+ databus.ps.publish("version", rtu_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)
- dbus_iface.ps.publish("has_modem", has_modem)
+ databus.ps.publish("has_modem", has_modem)
end
-- transmit unit hardware type across the bus
---@param uid integer unit ID
---@param type RTU_UNIT_TYPE
function databus.tx_unit_hw_type(uid, type)
- dbus_iface.ps.publish("unit_type_" .. uid, type)
+ databus.ps.publish("unit_type_" .. uid, type)
end
-- transmit unit hardware status across the bus
---@param uid integer unit ID
---@param status RTU_UNIT_HW_STATE
function databus.tx_unit_hw_status(uid, status)
- dbus_iface.ps.publish("unit_hw_" .. uid, status)
+ databus.ps.publish("unit_hw_" .. uid, status)
end
-- transmit thread (routine) statuses
---@param thread string thread name
---@param ok boolean thread state
function databus.tx_rt_status(thread, ok)
- dbus_iface.ps.publish(util.c("routine__", thread), ok)
+ databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
---@param state integer
function databus.tx_link_state(state)
- dbus_iface.ps.publish("link_state", state)
+ databus.ps.publish("link_state", state)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
- dbus_iface.ps.subscribe(field, func)
+ databus.ps.subscribe(field, func)
end
return databus
diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua
index 8dcaa1f..4c4aa78 100644
--- a/rtu/panel/front_panel.lua
+++ b/rtu/panel/front_panel.lua
@@ -49,22 +49,22 @@ local function init(panel, units)
on.update(true)
system.line_break()
- databus.rx_field("heartbeat", heartbeat.update)
+ heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(5)
system.line_break()
- databus.rx_field("has_modem", modem.update)
- databus.rx_field("link_state", network.update)
+ modem.register(databus.ps, "has_modem", modem.update)
+ network.register(databus.ps, "link_state", network.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
local rt_comm = LED{parent=system,label="RT COMMS",colors=cpair(colors.green,colors.green_off)}
system.line_break()
- databus.rx_field("routine__main", rt_main.update)
- databus.rx_field("routine__comms", rt_comm.update)
+ rt_main.register(databus.ps, "routine__main", rt_main.update)
+ rt_comm.register(databus.ps, "routine__comms", rt_comm.update)
--
-- about label
@@ -74,8 +74,8 @@ local function init(panel, units)
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}
- databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
- databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
+ 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)
--
-- unit status list
@@ -90,7 +90,7 @@ local function init(panel, units)
for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=cpair(colors.green,colors.green_off)}
- databus.rx_field("routine__unit_" .. i, rt_unit.update)
+ rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3}
@@ -102,13 +102,13 @@ local function init(panel, units)
-- hardware status
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
- databus.rx_field("unit_hw_" .. i, unit_hw.update)
+ unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index)
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.index)
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1}
- databus.rx_field("unit_type_" .. i, function (t)
+ name_box.register(databus.ps, "unit_type_" .. i, function (t)
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
end)
diff --git a/rtu/renderer.lua b/rtu/renderer.lua
index 490959c..17949ce 100644
--- a/rtu/renderer.lua
+++ b/rtu/renderer.lua
@@ -45,10 +45,8 @@ function renderer.close_ui()
-- stop blinking indicators
flasher.clear()
- -- hide to stop animation callbacks
- ui.display.hide()
-
- -- clear root UI elements
+ -- delete element tree
+ ui.display.delete()
ui.display = nil
-- restore colors
diff --git a/rtu/startup.lua b/rtu/startup.lua
index 0693f9f..a284b4b 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.1.0"
+local RTU_VERSION = "v1.2.0"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
diff --git a/scada-common/psil.lua b/scada-common/psil.lua
index 664d10d..13dcaa5 100644
--- a/scada-common/psil.lua
+++ b/scada-common/psil.lua
@@ -2,6 +2,8 @@
-- Publisher-Subscriber Interconnect Layer
--
+local util = require("scada-common.util")
+
local psil = {}
-- instantiate a new PSI layer
@@ -36,6 +38,15 @@ function psil.create()
table.insert(self.ic[key].subscribers, { notify = func })
end
+ -- unsubscribe a function from a given key
+ ---@param key string data key
+ ---@param func function function to unsubscribe
+ function public.unsubscribe(key, func)
+ if self.ic[key] ~= nil then
+ util.filter_table(self.ic[key].subscribers, function (s) return s.notify ~= func end)
+ end
+ end
+
-- publish data to a given key, passing it to all subscribers if it has changed
---@param key string data key
---@param value any data value
@@ -64,6 +75,9 @@ function psil.create()
end
end
+ -- clear the contents of the interconnect
+ function public.purge() self.ic = nil end
+
return public
end