diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index edf0de8..f0563ca 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -171,15 +171,15 @@ end function renderer.ui_ready() return engine.ui_ready end -- handle a touch event ----@param event monitor_touch -function renderer.handle_touch(event) +---@param event mouse_interaction +function renderer.handle_mouse(event) if event.monitor == engine.monitors.primary_name then - ui.main_layout.handle_touch(event) + ui.main_layout.handle_mouse(event) else for id, monitor in pairs(engine.monitors.unit_name_map) do if event.monitor == monitor then local layout = ui.unit_layouts[id] ---@type graphics_element - layout.handle_touch(event) + layout.handle_mouse(event) end end end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index a6a81f1..95178ad 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") -local COORDINATOR_VERSION = "v0.12.3" +local COORDINATOR_VERSION = "v0.12.4" local print = util.print local println = util.println @@ -354,7 +354,7 @@ local function main() end elseif event == "monitor_touch" then -- handle a monitor touch event - renderer.handle_touch(core.events.touch(param1, param2, param3)) + renderer.handle_mouse(core.events.touch(param1, param2, param3)) elseif event == "speaker_audio_empty" then -- handle speaker buffer emptied sounder.continue() diff --git a/graphics/core.lua b/graphics/core.lua index 98c8ed5..d03e551 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -10,20 +10,76 @@ core.flasher = flasher local events = {} ----@class monitor_touch +---@enum click_type +events.click_type = { + VIRTUAL = 0, + LEFT_BUTTON = 1, + RIGHT_BUTTON = 2, + MID_BUTTON = 3 +} + +---@class mouse_interaction ---@field monitor string +---@field button integer ---@field x integer ---@field y integer --- create a new touch event definition +-- create a new monitor touch mouse interaction event ---@nodiscard ---@param monitor string ---@param x integer ---@param y integer ----@return monitor_touch +---@return mouse_interaction function events.touch(monitor, x, y) return { monitor = monitor, + button = events.click_type.LEFT_BUTTON, + x = x, + y = y + } +end + +-- create a new mouse click mouse interaction event +---@nodiscard +---@param button click_type +---@param x integer +---@param y integer +---@return mouse_interaction +function events.click(button, x, y) + return { + monitor = "terminal", + button = button, + x = x, + y = y + } +end + +-- create a new transposed mouse interaction event using the event's monitor/button fields +---@nodiscard +---@param event mouse_interaction +---@param new_x integer +---@param new_y integer +---@return mouse_interaction +function events.mouse_transposed(event, new_x, new_y) + return { + monitor = event.monitor, + button = event.button, + x = new_x, + y = new_y + } +end + +-- create a new generic mouse interaction event +---@nodiscard +---@param monitor string +---@param button click_type +---@param x integer +---@param y integer +---@return mouse_interaction +function events.mouse_generic(monitor, button, x, y) + return { + monitor = monitor, + button = button, x = x, y = y } diff --git a/graphics/element.lua b/graphics/element.lua index 8aa3ce9..d9bc489 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -32,6 +32,9 @@ local element = {} ---|data_indicator_args ---|hbar_args ---|icon_indicator_args +---|indicator_led_args +---|indicator_led_pair_args +---|indicator_led_rgb_args ---|indicator_light_args ---|power_indicator_args ---|rad_indicator_args @@ -100,7 +103,13 @@ function element.new(args) else local w, h = self.p_window.getSize() protected.frame.x = args.x or 1 - protected.frame.y = args.y or next_y + + if args.parent ~= nil then + protected.frame.y = args.y or (next_y - offset_y) + else + protected.frame.y = args.y or next_y + end + protected.frame.w = args.width or w protected.frame.h = args.height or h end @@ -157,9 +166,9 @@ function element.new(args) self.bounds.y2 = self.position.y + f.h - 1 end - -- handle a touch event - ---@param event table monitor_touch event - function protected.handle_touch(event) + -- handle a mouse event + ---@param event mouse_interaction mouse interaction event + function protected.handle_mouse(event) end -- handle data value changes @@ -260,6 +269,11 @@ function element.new(args) ---@param child graphics_template ---@return integer|string key function public.__add_child(key, child) + -- offset first automatic placement + if self.next_y <= self.child_offset.y then + self.next_y = self.child_offset.y + 1 + end + child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y) self.next_y = child.frame.y + child.frame.h @@ -396,20 +410,20 @@ function element.new(args) -- FUNCTION CALLBACKS -- - -- handle a monitor touch - ---@param event monitor_touch monitor touch event - function public.handle_touch(event) + -- handle a monitor touch or mouse click + ---@param event mouse_interaction mouse interaction event + function public.handle_mouse(event) local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2 local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2 if in_x and in_y then - local event_T = core.events.touch(event.monitor, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1) + local event_T = core.events.mouse_transposed(event, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1) -- handle the touch event, transformed into the window frame - protected.handle_touch(event_T) + protected.handle_mouse(event_T) -- pass on touch event to children - for _, val in pairs(self.children) do val.handle_touch(event_T) end + for _, val in pairs(self.children) do val.handle_mouse(event_T) end end end diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index 0b59df6..e9f2bf4 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -140,10 +140,10 @@ local function hazard_button(args) end end - -- handle touch - ---@param event monitor_touch monitor touch event + -- handle mouse interaction + ---@param event mouse_interaction mouse event ---@diagnostic disable-next-line: unused-local - function e.handle_touch(event) + function e.handle_mouse(event) if e.enabled then -- change text color to indicate clicked e.window.setTextColor(args.accent) @@ -178,7 +178,7 @@ local function hazard_button(args) -- set the value (true simulates pressing the button) ---@param val boolean new value function e.set_value(val) - if val then e.handle_touch(core.events.touch("", 1, 1)) end + if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end end -- show the button as disabled diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua index 2cf583a..2549e2b 100644 --- a/graphics/elements/controls/multi_button.lua +++ b/graphics/elements/controls/multi_button.lua @@ -92,9 +92,10 @@ local function multi_button(args) end end - -- handle touch - ---@param event monitor_touch monitor touch event - function e.handle_touch(event) + -- handle mouse interaction + ---@param event mouse_interaction mouse event +---@diagnostic disable-next-line: unused-local + function e.handle_mouse(event) -- determine what was pressed if e.enabled and event.y == 1 then for i = 1, #args.options do diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index 8cb89c9..d0c1299 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -8,7 +8,7 @@ local element = require("graphics.element") ---@class push_button_args ---@field text string button text ---@field callback function function to call on touch ----@field min_width? integer text length + 2 if omitted +---@field min_width? integer text length if omitted ---@field active_fg_bg? cpair foreground/background colors when pressed ---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field parent graphics_element @@ -47,10 +47,10 @@ local function push_button(args) e.window.write(args.text) end - -- handle touch - ---@param event monitor_touch monitor touch event + -- handle mouse interaction + ---@param event mouse_interaction mouse event ---@diagnostic disable-next-line: unused-local - function e.handle_touch(event) + function e.handle_mouse(event) if e.enabled then if args.active_fg_bg ~= nil then -- show as pressed @@ -78,7 +78,7 @@ local function push_button(args) -- set the value (true simulates pressing the button) ---@param val boolean new value function e.set_value(val) - if val then e.handle_touch(core.events.touch("", 1, 1)) end + if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end end -- show butten as enabled diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 025cad1..3b2a593 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -79,9 +79,9 @@ local function radio_button(args) end end - -- handle touch - ---@param event monitor_touch monitor touch event - function e.handle_touch(event) + -- handle mouse interaction + ---@param event mouse_interaction mouse event + function e.handle_mouse(event) -- determine what was pressed if e.enabled then if args.options[event.y] ~= nil then diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua index 088d847..ffbd1f8 100644 --- a/graphics/elements/controls/spinbox_numeric.lua +++ b/graphics/elements/controls/spinbox_numeric.lua @@ -127,9 +127,9 @@ local function spinbox(args) -- init with the default value show_num() - -- handle touch - ---@param event monitor_touch monitor touch event - function e.handle_touch(event) + -- handle mouse interaction + ---@param event mouse_interaction mouse event + function e.handle_mouse(event) -- only handle if on an increment or decrement arrow if e.enabled and event.x ~= dec_point_x then local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x) diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua index bf138f2..133ea45 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -62,10 +62,10 @@ local function switch_button(args) -- initial draw draw_state() - -- handle touch - ---@param event monitor_touch monitor touch event + -- handle mouse interaction + ---@param event mouse_interaction mouse event ---@diagnostic disable-next-line: unused-local - function e.handle_touch(event) + function e.handle_mouse(event) if e.enabled then -- toggle state e.value = not e.value diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua new file mode 100644 index 0000000..7905848 --- /dev/null +++ b/graphics/elements/indicators/led.lua @@ -0,0 +1,98 @@ +-- Indicator "LED" Graphics Element + +local util = require("scada-common.util") + +local element = require("graphics.element") +local flasher = require("graphics.flasher") + +---@class indicator_led_args +---@field label string indicator label +---@field colors cpair on/off colors (a/b respectively) +---@field min_label_width? integer label length if omitted +---@field flash? boolean whether to flash on true rather than stay on +---@field period? PERIOD flash period +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@field fg_bg? cpair foreground/background colors + +-- new indicator LED +---@nodiscard +---@param args indicator_led_args +---@return graphics_element element, element_id id +local function indicator_led(args) + assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field") + assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field") + + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled") + end + + -- single line + args.height = 1 + + -- determine width + args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + + -- flasher state + local flash_on = true + + -- create new graphics element base object + local e = element.new(args) + + -- called by flasher when enabled + local function flash_callback() + e.window.setCursorPos(1, 1) + + if flash_on then + e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) + else + e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) + end + + flash_on = not flash_on + end + + -- enable light or start flashing + local function enable() + if args.flash then + flash_on = true + flasher.start(flash_callback, args.period) + else + e.window.setCursorPos(1, 1) + e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) + end + end + + -- disable light or stop flashing + local function disable() + if args.flash then + flash_on = false + flasher.stop(flash_callback) + end + + e.window.setCursorPos(1, 1) + e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) + end + + -- on state change + ---@param new_state boolean indicator state + function e.on_update(new_state) + e.value = new_state + if new_state then enable() else disable() end + end + + -- set indicator state + ---@param val boolean indicator state + function e.set_value(val) e.on_update(val) end + + -- write label and initial indicator light + e.on_update(false) + e.window.setCursorPos(3, 1) + e.window.write(args.label) + + return e.get() +end + +return indicator_led diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua new file mode 100644 index 0000000..aaf94ec --- /dev/null +++ b/graphics/elements/indicators/ledpair.lua @@ -0,0 +1,112 @@ +-- Indicator LED Pair Graphics Element (two LEDs provide: off, color_a, color_b) + +local util = require("scada-common.util") + +local element = require("graphics.element") +local flasher = require("graphics.flasher") + +---@class indicator_led_pair_args +---@field label string indicator label +---@field off color color for off +---@field c1 color color for #1 on +---@field c2 color color for #2 on +---@field min_label_width? integer label length if omitted +---@field flash? boolean whether to flash when on rather than stay on +---@field period? PERIOD flash period +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@field fg_bg? cpair foreground/background colors + +-- new dual LED indicator light +---@nodiscard +---@param args indicator_led_pair_args +---@return graphics_element element, element_id id +local function indicator_led_pair(args) + assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field") + assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field") + assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field") + assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field") + + if args.flash then + assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled") + end + + -- single line + args.height = 1 + + -- determine width + args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + + -- flasher state + local flash_on = true + + -- blit translations + local co = colors.toBlit(args.off) + local c1 = colors.toBlit(args.c1) + local c2 = colors.toBlit(args.c2) + + -- create new graphics element base object + local e = element.new(args) + + -- init value for initial check in on_update + e.value = 1 + + -- called by flasher when enabled + local function flash_callback() + e.window.setCursorPos(1, 1) + + if flash_on then + if e.value == 2 then + e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) + elseif e.value == 3 then + e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) + end + else + e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + end + + flash_on = not flash_on + end + + -- on state change + ---@param new_state integer indicator state + function e.on_update(new_state) + local was_off = e.value <= 1 + + e.value = new_state + e.window.setCursorPos(1, 1) + + if args.flash then + if was_off and (new_state > 1) then + flash_on = true + flasher.start(flash_callback, args.period) + elseif new_state <= 1 then + flash_on = false + flasher.stop(flash_callback) + + e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + end + elseif new_state == 2 then + e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) + elseif new_state == 3 then + e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) + else + e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + end + end + + -- set indicator state + ---@param val integer indicator state + function e.set_value(val) e.on_update(val) end + + -- write label and initial indicator light + e.on_update(1) + e.window.setCursorPos(3, 1) + e.window.write(args.label) + + return e.get() +end + +return indicator_led_pair diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua new file mode 100644 index 0000000..c58b835 --- /dev/null +++ b/graphics/elements/indicators/ledrgb.lua @@ -0,0 +1,57 @@ +-- Indicator RGB LED Graphics Element + +local element = require("graphics.element") + +---@class indicator_led_rgb_args +---@field label string indicator label +---@field colors table colors to use +---@field min_label_width? integer label length if omitted +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@field fg_bg? cpair foreground/background colors + +-- new RGB LED indicator light +---@nodiscard +---@param args indicator_led_rgb_args +---@return graphics_element element, element_id id +local function indicator_led_rgb(args) + assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field") + assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field") + + -- single line + args.height = 1 + + -- determine width + args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2 + + -- create new graphics element base object + local e = element.new(args) + + -- init value for initial check in on_update + e.value = 1 + + -- on state change + ---@param new_state integer indicator state + function e.on_update(new_state) + e.value = new_state + e.window.setCursorPos(1, 1) + if type(args.colors[new_state]) == "number" then + e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg) + end + end + + -- set indicator state + ---@param val integer indicator state + function e.set_value(val) e.on_update(val) end + + -- write label and initial indicator light + e.on_update(1) + e.window.setCursorPos(3, 1) + e.window.write(args.label) + + return e.get() +end + +return indicator_led_rgb diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua index 6422cbc..a4d8dde 100644 --- a/graphics/elements/rectangle.lua +++ b/graphics/elements/rectangle.lua @@ -7,6 +7,7 @@ local element = require("graphics.element") ---@class rectangle_args ---@field border? graphics_border ---@field thin? boolean true to use extra thin even borders +---@field even_inner? boolean true to make the inner area of a border even ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -66,14 +67,27 @@ local function rectangle(args) local blit_bg_top_bot = util.strrep(border_blit, e.frame.w) -- partial bars - local p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width) - local p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width) - local p_s = spaces - + local p_a, p_b, p_s if args.thin == true then - p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94" - p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85" + if args.even_inner == true then + p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93" + p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e" + else + p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94" + p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85" + end + p_s = "\x95" .. util.spaces(inner_width) .. "\x95" + else + if args.even_inner == true then + p_a = util.strrep("\x83", inner_width + width_x2) + p_b = util.strrep("\x8f", inner_width + width_x2) + else + p_a = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width) + p_b = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width) + end + + p_s = spaces end local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. @@ -112,10 +126,13 @@ local function rectangle(args) if args.thin == true then e.window.blit(p_a, p_inv_bg, p_inv_fg) else + local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg) + local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) + if width_x2 % 3 == 1 then - e.window.blit(p_b, p_inv_bg, p_inv_fg) + e.window.blit(p_b, _fg, _bg) elseif width_x2 % 3 == 2 then - e.window.blit(p_a, p_inv_bg, p_inv_fg) + e.window.blit(p_a, _fg, _bg) else -- skip line e.window.blit(spaces, blit_fg, blit_bg_sides) @@ -129,12 +146,19 @@ local function rectangle(args) -- partial pixel fill if args.border.even and y == ((e.frame.h - border_width) + 1) then if args.thin == true then - e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + if args.even_inner == true then + e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w)) + else + e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + end else + local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) + local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + if width_x2 % 3 == 1 then - e.window.blit(p_a, p_inv_fg, blit_bg_top_bot) - elseif width_x2 % 3 == 2 or (args.thin == true) then - e.window.blit(p_b, p_inv_fg, blit_bg_top_bot) + e.window.blit(p_a, _fg, _bg) + elseif width_x2 % 3 == 2 then + e.window.blit(p_b, _fg, _bg) else -- skip line e.window.blit(spaces, blit_fg, blit_bg_sides) diff --git a/imgen.py b/imgen.py index 8b61d7d..30e7e69 100644 --- a/imgen.py +++ b/imgen.py @@ -68,7 +68,7 @@ def make_manifest(size): "pocket" : list_files("./pocket"), }, "depends" : { - "reactor-plc" : [ "system", "common" ], + "reactor-plc" : [ "system", "common", "graphics" ], "rtu" : [ "system", "common" ], "supervisor" : [ "system", "common" ], "coordinator" : [ "system", "common", "graphics" ], diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua new file mode 100644 index 0000000..eeb2260 --- /dev/null +++ b/reactor-plc/databus.lua @@ -0,0 +1,101 @@ +-- +-- Data Bus - Central Communication Linking for PLC Front Panel +-- + +local log = require("scada-common.log") +local psil = require("scada-common.psil") +local util = require("scada-common.util") + +local databus = {} + +local dbus_iface = { + ps = psil.create(), + rps_scram = function () log.debug("DBUS: unset rps_scram() called") end, + rps_reset = function () log.debug("DBUS: unset rps_reset() called") end +} + +-- call to toggle heartbeat signal +function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end + +-- link RPS command functions +---@param scram function reactor SCRAM function +---@param reset function RPS reset function +function databus.link_rps(scram, reset) + dbus_iface.rps_scram = scram + dbus_iface.rps_reset = reset +end + +-- transmit a command to the RPS to SCRAM +function databus.rps_scram() dbus_iface.rps_scram() end + +-- transmit a command to the RPS to reset +function databus.rps_reset() dbus_iface.rps_reset() end + +-- transmit firmware versions across the bus +---@param plc_v string PLC version +---@param comms_v string comms version +function databus.tx_versions(plc_v, comms_v) + dbus_iface.ps.publish("version", plc_v) + dbus_iface.ps.publish("comms_version", comms_v) +end + +-- transmit unit ID across the bus +---@param id integer unit ID +function databus.tx_id(id) + dbus_iface.ps.publish("unit_id", id) +end + +-- transmit hardware status across the bus +---@param plc_state plc_state +function databus.tx_hw_status(plc_state) + dbus_iface.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2))) + dbus_iface.ps.publish("has_modem", not plc_state.no_modem) + dbus_iface.ps.publish("degraded", plc_state.degraded) + dbus_iface.ps.publish("init_ok", plc_state.init_ok) +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 + +-- transmit reactor enable state across the bus +---@param active boolean reactor active +function databus.tx_reactor_state(active) + dbus_iface.ps.publish("reactor_active", active) +end + +-- transmit RPS data across the bus +---@param tripped boolean RPS tripped +---@param status table RPS status +function databus.tx_rps(tripped, status) + dbus_iface.ps.publish("rps_scram", tripped) + dbus_iface.ps.publish("rps_damage", status[1]) + dbus_iface.ps.publish("rps_high_temp", status[2]) + dbus_iface.ps.publish("rps_low_ccool", status[3]) + dbus_iface.ps.publish("rps_high_waste", status[4]) + dbus_iface.ps.publish("rps_high_hcool", status[5]) + dbus_iface.ps.publish("rps_no_fuel", status[6]) + dbus_iface.ps.publish("rps_fault", status[7]) + dbus_iface.ps.publish("rps_timeout", status[8]) + dbus_iface.ps.publish("rps_manual", status[9]) + dbus_iface.ps.publish("rps_automatic", status[10]) + dbus_iface.ps.publish("rps_sysfail", status[11]) +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/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua new file mode 100644 index 0000000..f875b5d --- /dev/null +++ b/reactor-plc/panel/front_panel.lua @@ -0,0 +1,124 @@ +-- +-- Main SCADA Coordinator GUI +-- + +local util = require("scada-common.util") + +local databus = require("reactor-plc.databus") + +local style = require("reactor-plc.panel.style") + +local core = require("graphics.core") +local flasher = require("graphics.flasher") + +local DisplayBox = require("graphics.elements.displaybox") +local Div = require("graphics.elements.div") +local Rectangle = require("graphics.elements.rectangle") +local TextBox = require("graphics.elements.textbox") + +local PushButton = require("graphics.elements.controls.push_button") + +local LED = require("graphics.elements.indicators.led") +local LEDPair = require("graphics.elements.indicators.ledpair") +local RGBLED = require("graphics.elements.indicators.ledrgb") + +local TEXT_ALIGN = core.graphics.TEXT_ALIGN + +local cpair = core.graphics.cpair +local border = core.graphics.border + +-- create new main view +---@param monitor table main viewscreen +local function init(monitor) + local panel = DisplayBox{window=monitor,fg_bg=style.root} + + local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + databus.rx_field("unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end) + + local system = Div{parent=panel,width=14,height=18,x=2,y=3} + + local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + system.line_break() + + databus.rx_field("init_ok", init_ok.update) + databus.rx_field("heartbeat", heartbeat.update) + + local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} + 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("reactor_dev_state", reactor.update) + 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_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)} + local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)} + local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.green_off)} + local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)} + system.line_break() + + databus.rx_field("routine__main", rt_main.update) + databus.rx_field("routine__rps", rt_rps.update) + databus.rx_field("routine__comms_tx", rt_cmtx.update) + databus.rx_field("routine__comms_rx", rt_cmrx.update) + databus.rx_field("routine__spctl", rt_sctl.update) + + local status = Div{parent=panel,width=19,height=18,x=17,y=3} + + local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)} + + local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,y=2,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)} + local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS} + + local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,y=5,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)} + local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)} + PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)} + PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)} + + databus.rx_field("reactor_active", active.update) + databus.rx_field("rps_scram", scram.update) + + local about = Rectangle{parent=panel,width=32,height=3,x=2,y=16,border=border(1,colors.ivory),thin=true,fg_bg=cpair(colors.black,colors.white)} + local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} + local comms_v = TextBox{parent=about,x=17,y=1,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) + + local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)} + local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)} + local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)} + local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)} + local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)} + local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)} + rps.line_break() + local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)} + local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)} + rps.line_break() + local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)} + local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)} + rps.line_break() + local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)} + local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)} + + databus.rx_field("rps_manual", rps_man.update) + databus.rx_field("rps_automatic", rps_auto.update) + databus.rx_field("rps_timeout", rps_tmo.update) + databus.rx_field("rps_fault", rps_flt.update) + databus.rx_field("rps_sysfail", rps_fail.update) + databus.rx_field("rps_damage", rps_dmg.update) + databus.rx_field("rps_high_temp", rps_tmp.update) + databus.rx_field("rps_no_fuel", rps_nof.update) + databus.rx_field("rps_high_waste", rps_wst.update) + databus.rx_field("rps_low_ccool", rps_ccl.update) + databus.rx_field("rps_high_hcool", rps_hcl.update) + + return panel +end + +return init diff --git a/reactor-plc/panel/style.lua b/reactor-plc/panel/style.lua new file mode 100644 index 0000000..01b00c9 --- /dev/null +++ b/reactor-plc/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 }, + { 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/reactor-plc/plc.lua b/reactor-plc/plc.lua index 6c2b066..2879290 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,10 +1,11 @@ -local comms = require("scada-common.comms") -local const = require("scada-common.constants") -local log = require("scada-common.log") -local ppm = require("scada-common.ppm") -local rsio = require("scada-common.rsio") -local types = require("scada-common.types") -local util = require("scada-common.util") +local comms = require("scada-common.comms") +local const = require("scada-common.constants") +local databus = require("reactor-plc.databus") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") local plc = {} @@ -19,11 +20,6 @@ local AUTO_ACK = comms.PLC_AUTO_ACK local RPS_LIMITS = const.RPS_LIMITS -local print = util.print -local println = util.println -local print_ts = util.print_ts -local println_ts = util.println_ts - -- I sure hope the devs don't change this error message, not that it would have safety implications -- I wish they didn't change it to be like this local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." @@ -389,6 +385,9 @@ function plc.rps_init(reactor, is_formed, emer_cool) -- update emergency coolant control if configured _set_emer_cool(self.state[state_keys.low_coolant]) + -- report RPS status + databus.tx_rps(self.tripped, self.state) + return self.tripped, status, first_trip end @@ -440,6 +439,9 @@ function plc.rps_init(reactor, is_formed, emer_cool) end end + -- link functions with databus + databus.link_rps(public.trip_manual, public.reset) + return public end @@ -776,6 +778,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, ---@param plc_state plc_state PLC state ---@param setpoints setpoints setpoint control table function public.handle_packet(packet, plc_state, setpoints) + -- print a log message to the terminal as long as the UI isn't running + local function println(message) if not plc_state.fp_ok then util.println(message) end end + local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end + + -- handle packets now that we have prints setup if packet.scada_frame.local_port() == local_port then -- check sequence number if self.r_seq_num == nil then @@ -962,6 +969,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- clear this since this is for something that was unsolicited self.last_est_ack = ESTABLISH_ACK.ALLOW + + -- report link state + databus.tx_link_state(est_ack + 1) else log.debug("SCADA_MGMT establish packet length mismatch") end @@ -1025,6 +1035,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, self.linked = est_ack == ESTABLISH_ACK.ALLOW 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/reactor-plc/renderer.lua b/reactor-plc/renderer.lua new file mode 100644 index 0000000..ad2bcc0 --- /dev/null +++ b/reactor-plc/renderer.lua @@ -0,0 +1,75 @@ +-- +-- Graphics Rendering Control +-- + +local style = require("reactor-plc.panel.style") +local panel_view = require("reactor-plc.panel.front_panel") + +local flasher = require("graphics.flasher") + +local renderer = {} + +local ui = { + view = nil +} + +-- start the UI +function renderer.start_ui() + if ui.view == 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.view = panel_view(term.current()) + end +end + +-- close out the UI +function renderer.close_ui() + -- stop blinking indicators + flasher.clear() + + if ui.view ~= nil then + -- hide to stop animation callbacks + ui.view.hide() + end + + -- clear root UI elements + ui.view = 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.view ~= nil end + +-- handle a mouse event +---@param event mouse_interaction +function renderer.handle_mouse(event) + ui.view.handle_mouse(event) +end + +return renderer diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index dc9d3ce..293281c 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -4,18 +4,21 @@ require("/initenv").init_env() -local crash = require("scada-common.crash") -local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") -local ppm = require("scada-common.ppm") -local rsio = require("scada-common.rsio") -local util = require("scada-common.util") +local comms = require("scada-common.comms") +local crash = require("scada-common.crash") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") -local config = require("reactor-plc.config") -local plc = require("reactor-plc.plc") -local threads = require("reactor-plc.threads") +local config = require("reactor-plc.config") +local databus = require("reactor-plc.databus") +local plc = require("reactor-plc.plc") +local renderer = require("reactor-plc.renderer") +local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.0.1" +local R_PLC_VERSION = "v1.1.4" local print = util.print local println = util.println @@ -71,6 +74,10 @@ local function main() -- startup ---------------------------------------- + -- record firmware versions and ID + databus.tx_versions(R_PLC_VERSION, comms.version) + databus.tx_id(config.REACTOR_ID) + -- mount connected devices ppm.mount_all() @@ -84,6 +91,7 @@ local function main() ---@class plc_state plc_state = { init_ok = true, + fp_ok = false, shutdown = false, degraded = false, reactor_formed = true, @@ -155,15 +163,31 @@ local function main() plc_state.no_modem = true end + -- print a log message to the terminal as long as the UI isn't running + local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end + -- PLC init
--- EVENT_CONSUMER: this function consumes events local function init() - if plc_state.init_ok then - -- just booting up, no fission allowed (neutrons stay put thanks) - if plc_state.reactor_formed and smem_dev.reactor.getStatus() then - smem_dev.reactor.scram() - end + -- just booting up, no fission allowed (neutrons stay put thanks) + if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then + smem_dev.reactor.scram() + end + -- front panel time! + if not renderer.ui_ready() then + local message = nil + plc_state.fp_ok, message = pcall(renderer.start_ui) + if not plc_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 + end + + if plc_state.init_ok then -- init reactor protection system smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed, config.EMERGENCY_COOL) log.debug("init> rps init") @@ -178,7 +202,7 @@ local function main() config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) log.debug("init> comms init") else - println("init> starting in offline mode") + _println_no_fp("init> starting in offline mode") log.info("init> running without networking") end @@ -190,12 +214,14 @@ local function main() util.push_event("clock_start") - println("init> completed") + _println_no_fp("init> completed") log.info("init> startup completed") else - println("init> system in degraded state, awaiting devices...") + _println_no_fp("init> system in degraded state, awaiting devices...") log.warning("init> started in a degraded state, awaiting peripheral connections...") end + + databus.tx_hw_status(plc_state) end ---------------------------------------- @@ -233,6 +259,8 @@ local function main() parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) end + renderer.close_ui() + println_ts("exited") log.info("exited") end diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index c288281..c307999 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -1,15 +1,16 @@ -local log = require("scada-common.log") -local mqueue = require("scada-common.mqueue") -local ppm = require("scada-common.ppm") -local util = require("scada-common.util") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local tcallbackdsp = require("scada-common.tcallbackdsp") +local util = require("scada-common.util") + +local databus = require("reactor-plc.databus") +local renderer = require("reactor-plc.renderer") + +local core = require("graphics.core") local threads = {} -local print = util.print -local println = util.println -local print_ts = util.print_ts -local println_ts = util.println_ts - local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local RPS_SLEEP = 250 -- (250ms, 5 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) @@ -32,11 +33,16 @@ local MQ__COMM_CMD = { ---@param smem plc_shared_memory ---@param init function function threads.thread__main(smem, init) + -- print a log message to the terminal as long as the UI isn't running + local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end + local function println_ts(message) if not smem.plc_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 init, clock inactive") -- send status updates at 2Hz (every 10 server ticks) (every loop tick) @@ -61,6 +67,9 @@ function threads.thread__main(smem, init) -- handle event if event == "timer" and loop_clock.is_clock(param1) then + -- blink heartbeat indicator + databus.heartbeat() + -- core clock tick if networked then -- start next clock timer @@ -133,6 +142,9 @@ function threads.thread__main(smem, init) -- reactor no longer formed plc_state.reactor_formed = false end + + -- update indicators + databus.tx_hw_status(plc_state) elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then -- got a packet local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) @@ -144,6 +156,9 @@ function threads.thread__main(smem, init) -- haven't heard from server recently? shutdown reactor plc_comms.unlink() smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) + elseif event == "timer" then + -- notify timer callback dispatcher if no other timer case claimed this event + tcallbackdsp.handle(param1) elseif event == "peripheral_detach" then -- peripheral disconnect local type, device = ppm.handle_unmount(param1) @@ -174,6 +189,9 @@ function threads.thread__main(smem, init) end end end + + -- update indicators + databus.tx_hw_status(plc_state) elseif event == "peripheral" then -- peripheral connect local type, device = ppm.mount(param1) @@ -237,6 +255,12 @@ function threads.thread__main(smem, init) plc_state.init_ok = true init() end + + -- update indicators + databus.tx_hw_status(plc_state) + elseif event == "mouse_click" then + -- handle a monitor touch event + renderer.handle_mouse(core.events.click(param1, param2, param3)) elseif event == "clock_start" then -- start loop clock loop_clock.start() @@ -263,6 +287,8 @@ function threads.thread__main(smem, init) log.fatal(util.strval(result)) end + databus.tx_rt_status("main", false) + -- if status is true, then we are probably exiting, so this won't matter -- if not, we need to restart the clock -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) @@ -280,11 +306,16 @@ end ---@nodiscard ---@param smem plc_shared_memory function threads.thread__rps(smem) + -- print a log message to the terminal as long as the UI isn't running + local function println(message) if not smem.plc_state.fp_ok then util.println(message) end end + local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end + ---@class parallel_thread local public = {} -- execute thread function public.exec() + databus.tx_rt_status("rps", true) log.debug("rps thread start") -- load in from shared memory @@ -317,11 +348,17 @@ function threads.thread__rps(smem) was_linked = true end - -- if we tried to SCRAM but failed, keep trying - -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) + if (not plc_state.no_reactor) and rps.is_formed() then + -- check reactor status ---@diagnostic disable-next-line: need-check-nil - if (not plc_state.no_reactor) and rps.is_formed() and rps.is_tripped() and reactor.getStatus() then - rps.scram() + local reactor_status = reactor.getStatus() + databus.tx_reactor_state(reactor_status) + + -- if we tried to SCRAM but failed, keep trying + -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) + if rps.is_tripped() and reactor_status then + rps.scram() + end end -- if we are in standalone mode, continuously reset RPS @@ -405,6 +442,8 @@ function threads.thread__rps(smem) log.fatal(util.strval(result)) end + databus.tx_rt_status("rps", false) + if not plc_state.shutdown then if plc_state.init_ok then smem.plc_sys.rps.scram() end log.info("rps thread restarting in 5 seconds...") @@ -425,6 +464,7 @@ function threads.thread__comms_tx(smem) -- execute thread function public.exec() + databus.tx_rt_status("comms_tx", true) log.debug("comms tx thread start") -- load in from shared memory @@ -482,6 +522,8 @@ function threads.thread__comms_tx(smem) log.fatal(util.strval(result)) end + databus.tx_rt_status("comms_tx", false) + if not plc_state.shutdown then log.info("comms tx thread restarting in 5 seconds...") util.psleep(5) @@ -501,6 +543,7 @@ function threads.thread__comms_rx(smem) -- execute thread function public.exec() + databus.tx_rt_status("comms_rx", true) log.debug("comms rx thread start") -- load in from shared memory @@ -558,6 +601,8 @@ function threads.thread__comms_rx(smem) log.fatal(util.strval(result)) end + databus.tx_rt_status("comms_rx", false) + if not plc_state.shutdown then log.info("comms rx thread restarting in 5 seconds...") util.psleep(5) @@ -577,6 +622,7 @@ function threads.thread__setpoint_control(smem) -- execute thread function public.exec() + databus.tx_rt_status("spctl", true) log.debug("setpoint control thread start") -- load in from shared memory @@ -691,6 +737,8 @@ function threads.thread__setpoint_control(smem) log.fatal(util.strval(result)) end + databus.tx_rt_status("spctl", false) + if not plc_state.shutdown then log.info("setpoint control thread restarting in 5 seconds...") util.psleep(5) diff --git a/scada-common/log.lua b/scada-common/log.lua index 424bf55..30f785d 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -16,7 +16,7 @@ local MODE = { log.MODE = MODE -- whether to log debug messages or not -local LOG_DEBUG = false +local LOG_DEBUG = true local log_sys = { path = "/log.txt", diff --git a/scada-common/psil.lua b/scada-common/psil.lua index c21b2cf..664d10d 100644 --- a/scada-common/psil.lua +++ b/scada-common/psil.lua @@ -51,6 +51,19 @@ function psil.create() self.ic[key].value = value end + -- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed
+ -- this is intended to be used to toggle boolean indicators such as heartbeats without extra state variables + ---@param key string data key + function public.toggle(key) + if self.ic[key] == nil then alloc(key) end + + self.ic[key].value = self.ic[key].value == false + + for i = 1, #self.ic[key].subscribers do + self.ic[key].subscribers[i].notify(self.ic[key].value) + end + end + return public end