From d143015cc7da2e4ee932bc21db9320796a161af2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 20 Apr 2023 20:40:28 -0400 Subject: [PATCH] #183 RTU front panel --- coordinator/startup.lua | 2 +- graphics/element.lua | 10 +- graphics/elements/indicators/led.lua | 8 +- graphics/elements/indicators/ledpair.lua | 8 +- graphics/elements/indicators/ledrgb.lua | 8 +- imgen.py | 4 +- install_manifest.json | 2 +- pocket/startup.lua | 2 +- reactor-plc/startup.lua | 2 +- rtu/databus.lua | 75 ++++++++++++++ rtu/panel/front_panel.lua | 123 +++++++++++++++++++++++ rtu/panel/style.lua | 41 ++++++++ rtu/renderer.lua | 81 +++++++++++++++ rtu/rtu.lua | 21 ++-- rtu/startup.lua | 44 +++++++- rtu/threads.lua | 85 +++++++++++++--- 16 files changed, 471 insertions(+), 45 deletions(-) create mode 100644 rtu/databus.lua create mode 100644 rtu/panel/front_panel.lua create mode 100644 rtu/panel/style.lua create mode 100644 rtu/renderer.lua diff --git a/coordinator/startup.lua b/coordinator/startup.lua index f232587..e2c7efe 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.13.3" +local COORDINATOR_VERSION = "v0.13.4" local println = util.println local println_ts = util.println_ts diff --git a/graphics/element.lua b/graphics/element.lua index 92b2f49..2265888 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -451,19 +451,13 @@ function element.new(args) function public.show() protected.window.setVisible(true) protected.start_anim() - - for i = 1, #self.children do - self.children[i].show() - end + for _, child in pairs(self.children) do child.show() end end -- hide the element function public.hide() protected.stop_anim() - for i = 1, #self.children do - self.children[i].hide() - end - + for _, child in pairs(self.children) do child.hide() end protected.window.setVisible(false) end diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua index 7905848..dd2264e 100644 --- a/graphics/elements/indicators/led.lua +++ b/graphics/elements/indicators/led.lua @@ -33,7 +33,7 @@ local function indicator_led(args) args.height = 1 -- determine width - args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 -- flasher state local flash_on = true @@ -89,8 +89,10 @@ local function indicator_led(args) -- write label and initial indicator light e.on_update(false) - e.window.setCursorPos(3, 1) - e.window.write(args.label) + if string.len(args.label) > 0 then + e.window.setCursorPos(3, 1) + e.window.write(args.label) + end return e.get() end diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua index aaf94ec..727d4cd 100644 --- a/graphics/elements/indicators/ledpair.lua +++ b/graphics/elements/indicators/ledpair.lua @@ -37,7 +37,7 @@ local function indicator_led_pair(args) args.height = 1 -- determine width - args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 -- flasher state local flash_on = true @@ -103,8 +103,10 @@ local function indicator_led_pair(args) -- write label and initial indicator light e.on_update(1) - e.window.setCursorPos(3, 1) - e.window.write(args.label) + if string.len(args.label) > 0 then + e.window.setCursorPos(3, 1) + e.window.write(args.label) + end return e.get() end diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua index e779785..266e0ab 100644 --- a/graphics/elements/indicators/ledrgb.lua +++ b/graphics/elements/indicators/ledrgb.lua @@ -24,7 +24,7 @@ local function indicator_led_rgb(args) args.height = 1 -- determine width - args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 -- create new graphics element base object local e = element.new(args) @@ -48,8 +48,10 @@ local function indicator_led_rgb(args) -- write label and initial indicator light e.on_update(1) - e.window.setCursorPos(3, 1) - e.window.write(args.label) + if string.len(args.label) > 0 then + e.window.setCursorPos(3, 1) + e.window.write(args.label) + end return e.get() end diff --git a/imgen.py b/imgen.py index 92d82db..9a1e194 100644 --- a/imgen.py +++ b/imgen.py @@ -70,7 +70,7 @@ def make_manifest(size): }, "depends" : { "reactor-plc" : [ "system", "common", "graphics" ], - "rtu" : [ "system", "common" ], + "rtu" : [ "system", "common", "graphics" ], "supervisor" : [ "system", "common" ], "coordinator" : [ "system", "common", "graphics" ], "pocket" : [ "system", "common", "graphics" ] @@ -108,7 +108,7 @@ f = open("install_manifest.json", "w") json.dump(final_manifest, f) f.close() -if sys.argv[1] == "shields": +if len(sys.argv) > 1 and sys.argv[1] == "shields": # write all the JSON files for shields.io for key, version in final_manifest["versions"].items(): f = open("./shields/" + key + ".json", "w") diff --git a/install_manifest.json b/install_manifest.json index b762a3e..c724f5b 100644 --- a/install_manifest.json +++ b/install_manifest.json @@ -1 +1 @@ -{"versions": {"installer": "v1.0", "bootloader": "0.2", "comms": "1.4.0", "reactor-plc": "v1.1.5", "rtu": "v0.13.3", "supervisor": "v0.14.4", "coordinator": "v0.12.6", "pocket": "alpha-v0.0.0"}, "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/flasher.lua", "graphics/core.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.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/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/threads.lua", "rtu/rtu.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.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/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.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/apisessions.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/unit_waiting.lua", "coordinator/ui/components/turbine.lua"], "pocket": ["pocket/config.lua", "pocket/startup.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 4909, "system": 1991, "common": 90041, "graphics": 111156, "lockbox": 100797, "reactor-plc": 94599, "rtu": 86149, "supervisor": 273852, "coordinator": 180746, "pocket": 347}} \ No newline at end of file +{"versions": {"installer": "v1.0", "bootloader": "0.2", "comms": "1.4.1", "reactor-plc": "v1.1.11", "rtu": "v1.0.0", "supervisor": "v0.15.3", "coordinator": "v0.13.4", "pocket": "alpha-v0.2.3"}, "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/flasher.lua", "graphics/core.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/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/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.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/turbine_page.lua", "pocket/ui/components/reactor_page.lua", "pocket/ui/components/home_page.lua", "pocket/ui/components/unit_page.lua", "pocket/ui/components/boiler_page.lua", "pocket/ui/components/conn_waiting.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common", "graphics"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 5467, "system": 1991, "common": 90336, "graphics": 115781, "lockbox": 100797, "reactor-plc": 95220, "rtu": 102446, "supervisor": 282665, "coordinator": 196044, "pocket": 35993}} \ No newline at end of file diff --git a/pocket/startup.lua b/pocket/startup.lua index b57756c..63ba41e 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.2.2" +local POCKET_VERSION = "alpha-v0.2.3" local println = util.println local println_ts = util.println_ts diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 624b78b..cb35f5e 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.1.10" +local R_PLC_VERSION = "v1.1.11" local println = util.println local println_ts = util.println_ts diff --git a/rtu/databus.lua b/rtu/databus.lua new file mode 100644 index 0000000..8ef720e --- /dev/null +++ b/rtu/databus.lua @@ -0,0 +1,75 @@ +-- +-- Data Bus - Central Communication Linking for RTU Front Panel +-- + +local psil = require("scada-common.psil") +local util = require("scada-common.util") + +local databus = {} + +local dbus_iface = { + ps = psil.create() +} + +---@enum RTU_UNIT_HW_STATE +local RTU_UNIT_HW_STATE = { + OFFLINE = 1, + FAULTED = 2, + UNFORMED = 3, + OK = 4 +} + +databus.RTU_UNIT_HW_STATE = RTU_UNIT_HW_STATE + +-- call to toggle heartbeat signal +function databus.heartbeat() dbus_iface.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) +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) +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) +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) +end + +-- transmit thread (routine) statuses +---@param thread string thread name +---@param ok boolean thread state +function databus.tx_rt_status(thread, ok) + dbus_iface.ps.publish(util.c("routine__", thread), ok) +end + +-- transmit supervisor link state across the bus +---@param state integer +function databus.tx_link_state(state) + dbus_iface.ps.publish("link_state", state) +end + +-- link a function to receive data from the bus +---@param field string field name +---@param func function function to link +function databus.rx_field(field, func) + dbus_iface.ps.subscribe(field, func) +end + +return databus diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua new file mode 100644 index 0000000..9bde6c7 --- /dev/null +++ b/rtu/panel/front_panel.lua @@ -0,0 +1,123 @@ +-- +-- Main SCADA Coordinator GUI +-- + +local util = require("scada-common.util") + +local databus = require("rtu.databus") + +local style = require("rtu.panel.style") + +local core = require("graphics.core") + +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") + +local LED = require("graphics.elements.indicators.led") +local RGBLED = require("graphics.elements.indicators.ledrgb") + +local TEXT_ALIGN = core.graphics.TEXT_ALIGN + +local cpair = core.graphics.cpair + +local UNIT_TYPE_LABELS = { + "UNKNOWN", + "REDSTONE", + "BOILER", + "TURBINE", + "IND MATRIX", + "SPS", + "SNA", + "ENV DETECTOR" +} + + +-- create new main view +---@param panel table main displaybox +---@param units table unit list +local function init(panel, units) + TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + + -- + -- system indicators + -- + + local system = Div{parent=panel,width=14,height=18,x=2,y=3} + + local on = LED{parent=system,label="POWER",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() + + databus.rx_field("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) + + 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) + + -- + -- about label + -- + + local about = Div{parent=panel,width=15,height=3,x=1,y=18,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} + + 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) + + -- + -- unit status list + -- + + local threads = Div{parent=panel,width=8,height=18,x=17,y=3} + + -- display up to 16 units + local list_length = math.min(#units, 16) + + -- show routine statuses + 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) + end + + local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3} + + -- show hardware statuses + for i = 1, list_length do + local unit = units[i] ---@type rtu_unit_registry_entry + + -- 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 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.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index)) + end) + + -- assignment (unit # or facility) + local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) + TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=cpair(colors.lightGray,colors.ivory)} + end + + return panel +end + +return init diff --git a/rtu/panel/style.lua b/rtu/panel/style.lua new file mode 100644 index 0000000..31039d4 --- /dev/null +++ b/rtu/panel/style.lua @@ -0,0 +1,41 @@ +-- +-- Graphics Style Options +-- + +local core = require("graphics.core") + +local style = {} + +local cpair = core.graphics.cpair + +-- GLOBAL -- + +-- remap global colors +colors.ivory = colors.pink +colors.red_off = colors.brown +colors.yellow_off = colors.magenta +colors.green_off = colors.lime + +style.root = cpair(colors.black, colors.ivory) +style.header = cpair(colors.black, colors.lightGray) + +style.colors = { + { c = colors.red, hex = 0xdf4949 }, -- RED ON + { c = colors.orange, hex = 0xffb659 }, + { c = colors.yellow, hex = 0xf9fb53 }, -- 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 = 0x0096ff }, + { c = colors.purple, hex = 0xb156ee }, + { c = colors.pink, hex = 0xdcd9ca }, -- IVORY + { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF + -- { c = colors.white, hex = 0xdcd9ca }, + { c = colors.lightGray, hex = 0xb1b8b3 }, + { c = colors.gray, hex = 0x575757 }, + -- { c = colors.black, hex = 0x191919 }, + { c = colors.brown, hex = 0x672223 } -- RED OFF +} + +return style diff --git a/rtu/renderer.lua b/rtu/renderer.lua new file mode 100644 index 0000000..0ac1d58 --- /dev/null +++ b/rtu/renderer.lua @@ -0,0 +1,81 @@ +-- +-- Graphics Rendering Control +-- + +local panel_view = require("rtu.panel.front_panel") +local style = require("rtu.panel.style") + +local flasher = require("graphics.flasher") + +local DisplayBox = require("graphics.elements.displaybox") + +local renderer = {} + +local ui = { + display = nil +} + +-- start the UI +---@param units table RTU units +function renderer.start_ui(units) + 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 + + -- start flasher callback task + flasher.run() + + -- init front panel view + ui.display = DisplayBox{window=term.current(),fg_bg=style.root} + panel_view(ui.display, units) + end +end + +-- close out the UI +function renderer.close_ui() + -- stop blinking indicators + flasher.clear() + + if ui.display ~= nil then + -- hide to stop animation callbacks + ui.display.hide() + end + + -- 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 + +-- 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 +function renderer.handle_mouse(event) + if ui.display ~= nil then + ui.display.handle_mouse(event) + end +end + +return renderer diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 2690f90..689d7c4 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,10 +1,11 @@ -local comms = require("scada-common.comms") -local ppm = require("scada-common.ppm") -local log = require("scada-common.log") -local types = require("scada-common.types") -local util = require("scada-common.util") +local comms = require("scada-common.comms") +local ppm = require("scada-common.ppm") +local log = require("scada-common.log") +local types = require("scada-common.types") +local util = require("scada-common.util") -local modbus = require("rtu.modbus") +local databus = require("rtu.databus") +local modbus = require("rtu.modbus") local rtu = {} @@ -14,8 +15,6 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local println_ts = util.println_ts - -- create a new RTU unit ---@nodiscard function rtu.init_unit() @@ -325,6 +324,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog ---@param units table RTU units ---@param rtu_state rtu_state function public.handle_packet(packet, units, rtu_state) + -- print a log message to the terminal as long as the UI isn't running + local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end + if packet.scada_frame.local_port() == local_port then -- check sequence number if self.r_seq_num == nil then @@ -416,6 +418,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog end self.last_est_ack = est_ack + + -- report link state + databus.tx_link_state(est_ack + 1) else log.debug("SCADA_MGMT establish packet length mismatch") end diff --git a/rtu/startup.lua b/rtu/startup.lua index 4551a24..d3c4971 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local comms = require("scada-common.comms") local crash = require("scada-common.crash") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") @@ -13,7 +14,9 @@ local types = require("scada-common.types") local util = require("scada-common.util") local config = require("rtu.config") +local databus = require("rtu.databus") local modbus = require("rtu.modbus") +local renderer = require("rtu.renderer") local rtu = require("rtu.rtu") local threads = require("rtu.threads") @@ -25,9 +28,10 @@ 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 = "v0.13.5" +local RTU_VERSION = "v1.0.0" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE +local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local println = util.println local println_ts = util.println_ts @@ -71,6 +75,9 @@ local function main() -- startup ---------------------------------------- + -- record firmware versions and ID + databus.tx_versions(RTU_VERSION, comms.version) + -- mount connected devices ppm.mount_all() @@ -79,6 +86,7 @@ local function main() -- RTU system state flags ---@class rtu_state rtu_state = { + fp_ok = false, linked = false, shutdown = false }, @@ -111,6 +119,8 @@ local function main() return end + databus.tx_hw_modem(true) + ---------------------------------------- -- interpret config and init units ---------------------------------------- @@ -250,6 +260,8 @@ local function main() log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message)) unit.uid = #units + + databus.tx_unit_hw_status(unit.uid, RTU_UNIT_HW_STATE.OK) end end @@ -409,6 +421,20 @@ local function main() log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message)) rtu_unit.uid = #units + + -- report hardware status + if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then + databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.OFFLINE) + else + if rtu_unit.is_multiblock then + databus.tx_unit_hw_status(rtu_unit.uid, util.trinary(rtu_unit.formed == true, RTU_UNIT_HW_STATE.OK, RTU_UNIT_HW_STATE.UNFORMED)) + elseif faulted then + databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.FAULTED) + else + databus.tx_unit_hw_status(rtu_unit.uid, RTU_UNIT_HW_STATE.OK) + end + end + end -- we made it through all that trusting-user-to-write-a-config-file chaos @@ -419,9 +445,23 @@ local function main() -- start system ---------------------------------------- + local rtu_state = __shared_memory.rtu_state + log.debug("boot> running configure()") if configure() then + -- start UI + local message + rtu_state.fp_ok, message = pcall(renderer.start_ui, units) + + if not rtu_state.fp_ok then + renderer.close_ui() + println_ts(util.c("UI error: ", message)) + println("init> running without front panel") + log.error(util.c("GUI crashed with error ", message)) + log.info("init> running in headless mode without front panel") + end + -- start connection watchdog smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) log.debug("startup> conn watchdog started") @@ -451,6 +491,8 @@ local function main() println("configuration failed, exiting...") end + renderer.close_ui() + println_ts("exited") log.info("exited") end diff --git a/rtu/threads.lua b/rtu/threads.lua index 7b2a6fa..9dac9f1 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -4,6 +4,10 @@ local ppm = require("scada-common.ppm") local types = require("scada-common.types") local util = require("scada-common.util") +local databus = require("rtu.databus") +local modbus = require("rtu.modbus") +local renderer = require("rtu.renderer") + local boilerv_rtu = require("rtu.dev.boilerv_rtu") local envd_rtu = require("rtu.dev.envd_rtu") local imatrix_rtu = require("rtu.dev.imatrix_rtu") @@ -11,13 +15,12 @@ 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 modbus = require("rtu.modbus") +local core = require("graphics.core") local threads = {} local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE - -local println_ts = util.println_ts +local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks) @@ -26,11 +29,15 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks) ---@nodiscard ---@param smem rtu_shared_memory function threads.thread__main(smem) + -- print a log message to the terminal as long as the UI isn't running + local function println_ts(message) if not smem.rtu_state.fp_ok then util.println_ts(message) end end + ---@class parallel_thread local public = {} -- execute thread function public.exec() + databus.tx_rt_status("main", true) log.debug("main thread start") -- main loop clock @@ -54,6 +61,9 @@ function threads.thread__main(smem) local event, param1, param2, param3, param4, param5 = util.pull_event() if event == "timer" and loop_clock.is_clock(param1) then + -- blink heartbeat indicator + databus.heartbeat() + -- start next clock timer loop_clock.start() @@ -82,6 +92,8 @@ function threads.thread__main(smem) if device == rtu_dev.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 @@ -91,10 +103,11 @@ function threads.thread__main(smem) if units[i].device == device then -- we are going to let the PPM prevent crashes -- return fault flags/codes to MODBUS queries - local unit = units[i] + local unit = units[i] ---@type rtu_unit_registry_entry local type_name = types.rtu_type_to_string(unit.type) println_ts(util.c("lost the ", type_name, " on interface ", unit.name)) log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name)) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OFFLINE) break end end @@ -113,6 +126,8 @@ function threads.thread__main(smem) println_ts("wireless modem reconnected.") log.info("comms modem reconnected") + + databus.tx_hw_modem(true) else log.info("wired modem reconnected") end @@ -153,34 +168,49 @@ function threads.thread__main(smem) resend_advert = false log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")")) end + + databus.tx_unit_hw_type(unit.uid, unit.type) end if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then unit.rtu = boilerv_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then unit.rtu = turbinev_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) elseif unit.type == RTU_UNIT_TYPE.IMATRIX then unit.rtu = imatrix_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) elseif unit.type == RTU_UNIT_TYPE.SPS then unit.rtu = sps_rtu.new(device) -- if not formed, indexing the multiblock functions would have resulted in a PPM fault unit.formed = util.trinary(device.__p_is_faulted(), false, nil) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) elseif unit.type == RTU_UNIT_TYPE.SNA then unit.rtu = sna_rtu.new(device) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then unit.rtu = envd_rtu.new(device) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) else log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) end - if unit.is_multiblock and (unit.formed == false) then - log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing")) + if unit.is_multiblock then + if (unit.formed == false) then + log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing")) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) + end + elseif device.__p_is_faulted() then + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.FAULTED) + else + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) end unit.modbus_io = modbus.new(unit.rtu, true) @@ -193,12 +223,15 @@ function threads.thread__main(smem) if resend_advert then rtu_comms.send_advertisement(units) else - rtu_comms.send_remounted(unit.uid) + rtu_comms.send_remounted(unit.uid) end end end end end + elseif event == "mouse_click" then + -- handle a monitor touch event + renderer.handle_mouse(core.events.click(param1, param2, param3)) end -- check for termination request @@ -220,6 +253,8 @@ function threads.thread__main(smem) log.fatal(util.strval(result)) end + databus.tx_rt_status("main", false) + if not rtu_state.shutdown then log.info("main thread restarting in 5 seconds...") util.psleep(5) @@ -239,6 +274,7 @@ function threads.thread__comms(smem) -- execute thread function public.exec() + databus.tx_rt_status("comms", true) log.debug("comms thread start") -- load in from shared memory @@ -294,6 +330,8 @@ function threads.thread__comms(smem) log.fatal(util.strval(result)) end + databus.tx_rt_status("comms", false) + if not rtu_state.shutdown then log.info("comms thread restarting in 5 seconds...") util.psleep(5) @@ -314,7 +352,8 @@ function threads.thread__unit_comms(smem, unit) -- execute thread function public.exec() - log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), "(", unit.name, ")")) + databus.tx_rt_status("unit_" .. unit.uid, true) + log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), " (", unit.name, ")")) -- load in from shared memory local rtu_state = smem.rtu_state @@ -348,6 +387,13 @@ function threads.thread__unit_comms(smem, unit) -- received a packet local _, reply = unit.modbus_io.handle_packet(msg.message) rtu_comms.send_modbus(reply) + + -- check if there was a problem and update the hardware state if so + local frame = reply.get() + if unit.formed and (bit.band(frame.func_code, types.MODBUS_FCODE.ERROR_FLAG) ~= 0) and + (frame.data[1] == types.MODBUS_EXCODE.SERVER_DEVICE_FAIL) then + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.FAULTED) + end end end @@ -361,7 +407,14 @@ function threads.thread__unit_comms(smem, unit) last_f_check = util.time_ms() - if unit.formed == nil then unit.formed = is_formed end + if unit.formed == nil then + unit.formed = is_formed + if is_formed then databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) end + end + + if not unit.formed then + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) + end if (not unit.formed) and is_formed then -- newly re-formed @@ -400,21 +453,25 @@ function threads.thread__unit_comms(smem, unit) unit.formed = device.isFormed() unit.modbus_io = modbus.new(unit.rtu, true) else - log.error("illegal remount of non-multiblock RTU attempted for " .. short_name, true) + log.error("illegal remount of non-multiblock RTU or type change attempted for " .. short_name, true) end if unit.formed and faulted then -- something is still wrong = can't mark as formed yet unit.formed = false + log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing")) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.UNFORMED) else rtu_comms.send_remounted(unit.uid) + databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK) end + + local type_name = types.rtu_type_to_string(unit.type) + log.info(util.c("reconnected the ", type_name, " on interface ", unit.name)) else -- fully lost the peripheral now :( log.error(util.c(unit.name, " lost (failed reconnect)")) end - - log.info(util.c("reconnected the ", unit.type, " on interface ", unit.name)) else log.error("failed to get interface of previously connected RTU unit " .. detail_name, true) end @@ -444,8 +501,10 @@ function threads.thread__unit_comms(smem, unit) log.fatal(util.strval(result)) end + databus.tx_rt_status("unit_" .. unit.uid, false) + if not rtu_state.shutdown then - log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), "(", unit.name, " restarting in 5 seconds...")) + log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds...")) util.psleep(5) end end