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