#183 RTU front panel

This commit is contained in:
Mikayla Fischler 2023-04-20 20:40:28 -04:00
parent df45f6c984
commit d143015cc7
16 changed files with 471 additions and 45 deletions

View File

@ -20,7 +20,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.13.3" local COORDINATOR_VERSION = "v0.13.4"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -451,19 +451,13 @@ function element.new(args)
function public.show() function public.show()
protected.window.setVisible(true) protected.window.setVisible(true)
protected.start_anim() protected.start_anim()
for _, child in pairs(self.children) do child.show() end
for i = 1, #self.children do
self.children[i].show()
end
end end
-- hide the element -- hide the element
function public.hide() function public.hide()
protected.stop_anim() protected.stop_anim()
for i = 1, #self.children do for _, child in pairs(self.children) do child.hide() end
self.children[i].hide()
end
protected.window.setVisible(false) protected.window.setVisible(false)
end end

View File

@ -33,7 +33,7 @@ local function indicator_led(args)
args.height = 1 args.height = 1
-- determine width -- 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 -- flasher state
local flash_on = true local flash_on = true
@ -89,8 +89,10 @@ local function indicator_led(args)
-- write label and initial indicator light -- write label and initial indicator light
e.on_update(false) e.on_update(false)
if string.len(args.label) > 0 then
e.window.setCursorPos(3, 1) e.window.setCursorPos(3, 1)
e.window.write(args.label) e.window.write(args.label)
end
return e.get() return e.get()
end end

View File

@ -37,7 +37,7 @@ local function indicator_led_pair(args)
args.height = 1 args.height = 1
-- determine width -- 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 -- flasher state
local flash_on = true local flash_on = true
@ -103,8 +103,10 @@ local function indicator_led_pair(args)
-- write label and initial indicator light -- write label and initial indicator light
e.on_update(1) e.on_update(1)
if string.len(args.label) > 0 then
e.window.setCursorPos(3, 1) e.window.setCursorPos(3, 1)
e.window.write(args.label) e.window.write(args.label)
end
return e.get() return e.get()
end end

View File

@ -24,7 +24,7 @@ local function indicator_led_rgb(args)
args.height = 1 args.height = 1
-- determine width -- 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 -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@ -48,8 +48,10 @@ local function indicator_led_rgb(args)
-- write label and initial indicator light -- write label and initial indicator light
e.on_update(1) e.on_update(1)
if string.len(args.label) > 0 then
e.window.setCursorPos(3, 1) e.window.setCursorPos(3, 1)
e.window.write(args.label) e.window.write(args.label)
end
return e.get() return e.get()
end end

View File

@ -70,7 +70,7 @@ def make_manifest(size):
}, },
"depends" : { "depends" : {
"reactor-plc" : [ "system", "common", "graphics" ], "reactor-plc" : [ "system", "common", "graphics" ],
"rtu" : [ "system", "common" ], "rtu" : [ "system", "common", "graphics" ],
"supervisor" : [ "system", "common" ], "supervisor" : [ "system", "common" ],
"coordinator" : [ "system", "common", "graphics" ], "coordinator" : [ "system", "common", "graphics" ],
"pocket" : [ "system", "common", "graphics" ] "pocket" : [ "system", "common", "graphics" ]
@ -108,7 +108,7 @@ f = open("install_manifest.json", "w")
json.dump(final_manifest, f) json.dump(final_manifest, f)
f.close() 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 # write all the JSON files for shields.io
for key, version in final_manifest["versions"].items(): for key, version in final_manifest["versions"].items():
f = open("./shields/" + key + ".json", "w") f = open("./shields/" + key + ".json", "w")

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ local coreio = require("pocket.coreio")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

75
rtu/databus.lua Normal file
View File

@ -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

123
rtu/panel/front_panel.lua Normal file
View File

@ -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

41
rtu/panel/style.lua Normal file
View File

@ -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

81
rtu/renderer.lua Normal file
View File

@ -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

View File

@ -4,6 +4,7 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local rtu = {} local rtu = {}
@ -14,8 +15,6 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local println_ts = util.println_ts
-- create a new RTU unit -- create a new RTU unit
---@nodiscard ---@nodiscard
function rtu.init_unit() 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 units table RTU units
---@param rtu_state rtu_state ---@param rtu_state rtu_state
function public.handle_packet(packet, units, 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 if packet.scada_frame.local_port() == local_port then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
@ -416,6 +418,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
end end
self.last_est_ack = est_ack self.last_est_ack = est_ack
-- report link state
databus.tx_link_state(est_ack + 1)
else else
log.debug("SCADA_MGMT establish packet length mismatch") log.debug("SCADA_MGMT establish packet length mismatch")
end end

View File

@ -4,6 +4,7 @@
require("/initenv").init_env() require("/initenv").init_env()
local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
@ -13,7 +14,9 @@ local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("rtu.config") local config = require("rtu.config")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu") local rtu = require("rtu.rtu")
local threads = require("rtu.threads") 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 sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_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_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -71,6 +75,9 @@ local function main()
-- startup -- startup
---------------------------------------- ----------------------------------------
-- record firmware versions and ID
databus.tx_versions(RTU_VERSION, comms.version)
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
@ -79,6 +86,7 @@ local function main()
-- RTU system state flags -- RTU system state flags
---@class rtu_state ---@class rtu_state
rtu_state = { rtu_state = {
fp_ok = false,
linked = false, linked = false,
shutdown = false shutdown = false
}, },
@ -111,6 +119,8 @@ local function main()
return return
end end
databus.tx_hw_modem(true)
---------------------------------------- ----------------------------------------
-- interpret config and init units -- 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)) log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
unit.uid = #units unit.uid = #units
databus.tx_unit_hw_status(unit.uid, RTU_UNIT_HW_STATE.OK)
end end
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)) 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 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 end
-- we made it through all that trusting-user-to-write-a-config-file chaos -- we made it through all that trusting-user-to-write-a-config-file chaos
@ -419,9 +445,23 @@ local function main()
-- start system -- start system
---------------------------------------- ----------------------------------------
local rtu_state = __shared_memory.rtu_state
log.debug("boot> running configure()") log.debug("boot> running configure()")
if configure() then 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 -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
@ -451,6 +491,8 @@ local function main()
println("configuration failed, exiting...") println("configuration failed, exiting...")
end end
renderer.close_ui()
println_ts("exited") println_ts("exited")
log.info("exited") log.info("exited")
end end

View File

@ -4,6 +4,10 @@ local ppm = require("scada-common.ppm")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") 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 boilerv_rtu = require("rtu.dev.boilerv_rtu")
local envd_rtu = require("rtu.dev.envd_rtu") local envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_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 sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local modbus = require("rtu.modbus") local core = require("graphics.core")
local threads = {} local threads = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
local println_ts = util.println_ts
local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
@ -26,11 +29,15 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
---@nodiscard ---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
function threads.thread__main(smem) 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 ---@class parallel_thread
local public = {} local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("main", true)
log.debug("main thread start") log.debug("main thread start")
-- main loop clock -- main loop clock
@ -54,6 +61,9 @@ function threads.thread__main(smem)
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
if event == "timer" and loop_clock.is_clock(param1) then if event == "timer" and loop_clock.is_clock(param1) then
-- blink heartbeat indicator
databus.heartbeat()
-- start next clock timer -- start next clock timer
loop_clock.start() loop_clock.start()
@ -82,6 +92,8 @@ function threads.thread__main(smem)
if device == rtu_dev.modem then if device == rtu_dev.modem then
println_ts("wireless modem disconnected!") println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected!") log.warning("comms modem disconnected!")
databus.tx_hw_modem(false)
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@ -91,10 +103,11 @@ function threads.thread__main(smem)
if units[i].device == device then if units[i].device == device then
-- we are going to let the PPM prevent crashes -- we are going to let the PPM prevent crashes
-- return fault flags/codes to MODBUS queries -- 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) local type_name = types.rtu_type_to_string(unit.type)
println_ts(util.c("lost the ", type_name, " on interface ", unit.name)) 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)) 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 break
end end
end end
@ -113,6 +126,8 @@ function threads.thread__main(smem)
println_ts("wireless modem reconnected.") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected") log.info("comms modem reconnected")
databus.tx_hw_modem(true)
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end
@ -153,34 +168,49 @@ function threads.thread__main(smem)
resend_advert = false resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")")) log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end end
databus.tx_unit_hw_type(unit.uid, unit.type)
end end
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu = boilerv_rtu.new(device) unit.rtu = boilerv_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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 elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu = turbinev_rtu.new(device) unit.rtu = turbinev_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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 elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu = imatrix_rtu.new(device) unit.rtu = imatrix_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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 elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu = sps_rtu.new(device) unit.rtu = sps_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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 elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu = sna_rtu.new(device) 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 elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu = envd_rtu.new(device) unit.rtu = envd_rtu.new(device)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
else else
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end end
if unit.is_multiblock and (unit.formed == false) then 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")) 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 end
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
@ -199,6 +229,9 @@ function threads.thread__main(smem)
end 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 end
-- check for termination request -- check for termination request
@ -220,6 +253,8 @@ function threads.thread__main(smem)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("main", false)
if not rtu_state.shutdown then if not rtu_state.shutdown then
log.info("main thread restarting in 5 seconds...") log.info("main thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
@ -239,6 +274,7 @@ function threads.thread__comms(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms", true)
log.debug("comms thread start") log.debug("comms thread start")
-- load in from shared memory -- load in from shared memory
@ -294,6 +330,8 @@ function threads.thread__comms(smem)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("comms", false)
if not rtu_state.shutdown then if not rtu_state.shutdown then
log.info("comms thread restarting in 5 seconds...") log.info("comms thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
@ -314,7 +352,8 @@ function threads.thread__unit_comms(smem, unit)
-- execute thread -- execute thread
function public.exec() 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 -- load in from shared memory
local rtu_state = smem.rtu_state local rtu_state = smem.rtu_state
@ -348,6 +387,13 @@ function threads.thread__unit_comms(smem, unit)
-- received a packet -- received a packet
local _, reply = unit.modbus_io.handle_packet(msg.message) local _, reply = unit.modbus_io.handle_packet(msg.message)
rtu_comms.send_modbus(reply) 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
end end
@ -361,7 +407,14 @@ function threads.thread__unit_comms(smem, unit)
last_f_check = util.time_ms() 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 if (not unit.formed) and is_formed then
-- newly re-formed -- newly re-formed
@ -400,21 +453,25 @@ function threads.thread__unit_comms(smem, unit)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
else 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 end
if unit.formed and faulted then if unit.formed and faulted then
-- something is still wrong = can't mark as formed yet -- something is still wrong = can't mark as formed yet
unit.formed = false 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 else
rtu_comms.send_remounted(unit.uid) rtu_comms.send_remounted(unit.uid)
databus.tx_unit_hw_status(unit.uid, UNIT_HW_STATE.OK)
end end
local type_name = types.rtu_type_to_string(unit.type)
log.info(util.c("reconnected the ", type_name, " on interface ", unit.name))
else else
-- fully lost the peripheral now :( -- fully lost the peripheral now :(
log.error(util.c(unit.name, " lost (failed reconnect)")) log.error(util.c(unit.name, " lost (failed reconnect)"))
end end
log.info(util.c("reconnected the ", unit.type, " on interface ", unit.name))
else else
log.error("failed to get interface of previously connected RTU unit " .. detail_name, true) log.error("failed to get interface of previously connected RTU unit " .. detail_name, true)
end end
@ -444,8 +501,10 @@ function threads.thread__unit_comms(smem, unit)
log.fatal(util.strval(result)) log.fatal(util.strval(result))
end end
databus.tx_rt_status("unit_" .. unit.uid, false)
if not rtu_state.shutdown then 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) util.psleep(5)
end end
end end