From e4f49e994982607b0ed7480282d46726fbedae98 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 1 Sep 2023 14:23:39 +0000 Subject: [PATCH 01/35] #145 configure bootstrap command and size reduction of startup/initenv --- configure.lua | 16 ++++++++++++++++ imgen.py | 4 ++-- initenv.lua | 24 ++++++++---------------- startup.lua | 29 ++++++++--------------------- 4 files changed, 34 insertions(+), 39 deletions(-) create mode 100644 configure.lua diff --git a/configure.lua b/configure.lua new file mode 100644 index 0000000..20e67f3 --- /dev/null +++ b/configure.lua @@ -0,0 +1,16 @@ +print("CONFIGURE> SCANNING FOR CONFIGURATOR...") + +if fs.exists("reactor-plc/configure.lua") then + require("reactor-plc/configure.lua").configure() +elseif fs.exists("rtu/startup.lua") then + print("CONFIGURE> RTU CONFIGURATOR NOT YET IMPLEMENTED IN BETA") +elseif fs.exists("supervisor/startup.lua") then + print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA") +elseif fs.exists("coordinator/startup.lua") then + print("CONFIGURE> COORDINATOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA") +elseif fs.exists("pocket/startup.lua") then + print("CONFIGURE> POCKET CONFIGURATOR NOT YET IMPLEMENTED IN BETA") +else + print("CONFIGURE> NO CONFIGURATOR FOUND") + print("CONFIGURE> EXIT") +end diff --git a/imgen.py b/imgen.py index 8fc7989..31a61e0 100644 --- a/imgen.py +++ b/imgen.py @@ -60,7 +60,7 @@ def make_manifest(size): }, "files" : { # common files - "system" : [ "initenv.lua", "startup.lua" ], + "system" : [ "initenv.lua", "startup.lua", "configure.lua" ], "common" : list_files("./scada-common"), "graphics" : list_files("./graphics"), "lockbox" : list_files("./lockbox"), @@ -82,7 +82,7 @@ def make_manifest(size): # manifest file estimate "manifest" : size, # common files - "system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua"), + "system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"), "common" : dir_size("./scada-common"), "graphics" : dir_size("./graphics"), "lockbox" : dir_size("./lockbox"), diff --git a/initenv.lua b/initenv.lua index fffb705..05ad4f6 100644 --- a/initenv.lua +++ b/initenv.lua @@ -1,18 +1,10 @@ --- --- Initialize the Post-Boot Module Environment --- - return { - -- initialize booted environment - init_env = function () - local _require = require("cc.require") - local _env = setmetatable({}, { __index = _ENV }) - - -- overwrite require/package globals - require, package = _require.make(_env, "/") - - -- reset terminal - term.clear() - term.setCursorPos(1, 1) - end +-- initialize booted environment +init_env = function () + local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV }) + -- overwrite require/package globals + require, package = _require.make(_env, "/") + -- reset terminal + term.clear(); term.setCursorPos(1, 1) +end } diff --git a/startup.lua b/startup.lua index d312de3..d330066 100644 --- a/startup.lua +++ b/startup.lua @@ -1,6 +1,6 @@ local util = require("scada-common.util") -local BOOTLOADER_VERSION = "0.2" +local BOOTLOADER_VERSION = "0.3" local println = util.println local println_ts = util.println_ts @@ -12,39 +12,26 @@ local exit_code ---@type boolean println_ts("BOOT> SCANNING FOR APPLICATIONS...") if fs.exists("reactor-plc/startup.lua") then - -- found reactor-plc application - println("BOOT> FOUND REACTOR PLC APPLICATION") - println("BOOT> EXEC STARTUP") + println("BOOT> FOUND REACTOR PLC CODE: EXEC STARTUP") exit_code = shell.execute("reactor-plc/startup") elseif fs.exists("rtu/startup.lua") then - -- found rtu application - println("BOOT> FOUND RTU APPLICATION") - println("BOOT> EXEC STARTUP") + println("BOOT> FOUND RTU CODE: EXEC STARTUP") exit_code = shell.execute("rtu/startup") elseif fs.exists("supervisor/startup.lua") then - -- found supervisor application - println("BOOT> FOUND SUPERVISOR APPLICATION") - println("BOOT> EXEC STARTUP") + println("BOOT> FOUND SUPERVISOR CODE: EXEC STARTUP") exit_code = shell.execute("supervisor/startup") elseif fs.exists("coordinator/startup.lua") then - -- found coordinator application - println("BOOT> FOUND COORDINATOR APPLICATION") - println("BOOT> EXEC STARTUP") + println("BOOT> FOUND COORDINATOR CODE: EXEC STARTUP") exit_code = shell.execute("coordinator/startup") elseif fs.exists("pocket/startup.lua") then - -- found pocket application - println("BOOT> FOUND POCKET APPLICATION") - println("BOOT> EXEC STARTUP") + println("BOOT> FOUND POCKET CODE: EXEC STARTUP") exit_code = shell.execute("pocket/startup") else - -- no known applications found - println("BOOT> NO SCADA STARTUP APPLICATION FOUND") + println("BOOT> NO SCADA STARTUP FOUND") println("BOOT> EXIT") return false end -if not exit_code then - println_ts("BOOT> APPLICATION CRASHED") -end +if not exit_code then println_ts("BOOT> APPLICATION CRASHED") end return exit_code From 1cb240b1b0aa986db0e0cb1fdd2cb6fd4ce01e44 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Sep 2023 15:32:45 -0400 Subject: [PATCH 02/35] improved ignoring mouse events for hidden elements --- graphics/core.lua | 2 +- graphics/element.lua | 17 ++++++++++++++++- graphics/events.lua | 8 +++----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index 598c702..5177b8c 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "1.1.2" +core.version = "1.1.3" core.flasher = flasher core.events = events diff --git a/graphics/element.lua b/graphics/element.lua index 9308558..05fbb74 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -4,6 +4,8 @@ local core = require("graphics.core") +local events = core.events + local element = {} ---@class graphics_args_generic @@ -75,6 +77,7 @@ function element.new(args, child_offset_x, child_offset_y) next_y = 1, -- next child y coordinate next_id = 0, -- next child ID subscriptions = {}, + button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) }, mt = {} } @@ -550,12 +553,24 @@ function element.new(args, child_offset_x, child_offset_y) local ini_in = protected.in_window_bounds(x_ini, y_ini) if ini_in then - local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y) + if event.type == events.CLICK_TYPE.UP or event.type == events.CLICK_TYPE.DRAG then + -- make sure we don't handle mouse events that started before this element was made visible + if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then + return + end + elseif event.type == events.CLICK_TYPE.DOWN then + self.button_down[event.button] = event.initial + end + + local event_T = events.mouse_transposed(event, self.position.x, self.position.y) -- handle the mouse event then pass to children protected.handle_mouse(event_T) for _, child in pairs(protected.children) do child.handle_mouse(event_T) end end + elseif event.type == events.CLICK_TYPE.DOWN then + -- don't track this click + self.button_down[event.button] = events.new_coord_2d(-1, -1) end end diff --git a/graphics/events.lua b/graphics/events.lua index 3391a18..ca56588 100644 --- a/graphics/events.lua +++ b/graphics/events.lua @@ -30,6 +30,8 @@ events.CLICK_TYPE = { ---@return coordinate_2d local function _coord2d(x, y) return { x = x, y = y } end +events.new_coord_2d = _coord2d + ---@class mouse_interaction ---@field monitor string ---@field button CLICK_BUTTON @@ -39,11 +41,7 @@ local function _coord2d(x, y) return { x = x, y = y } end local handler = { -- left, right, middle button down tracking - button_down = { - _coord2d(0, 0), - _coord2d(0, 0), - _coord2d(0, 0) - } + button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) } } -- create a new monitor touch mouse interaction event From 29e910ba3c716f12526cd03125bc56936a661321 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 16 Sep 2023 21:06:16 -0400 Subject: [PATCH 03/35] #342 added element focusing feature to graphics library --- .vscode/settings.json | 1 + graphics/core.lua | 18 +- graphics/element.lua | 454 ++++++++++++------- graphics/elements/controls/app.lua | 10 +- graphics/elements/controls/hazard_button.lua | 2 +- graphics/elements/controls/push_button.lua | 28 +- graphics/elements/controls/sidebar.lua | 10 +- graphics/elements/form/number_field.lua | 136 ++++++ graphics/elements/listbox.lua | 14 +- graphics/events.lua | 89 +++- 10 files changed, 540 insertions(+), 222 deletions(-) create mode 100644 graphics/elements/form/number_field.lua diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e81f80..b5e7976 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "colors", "fs", "http", + "keys", "parallel", "periphemu", "peripheral", diff --git a/graphics/core.lua b/graphics/core.lua index 5177b8c..2305990 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "1.1.3" +core.version = "2.0.0" core.flasher = flasher core.events = events @@ -15,11 +15,7 @@ core.events = events -- Core Types ---@enum TEXT_ALIGN -core.TEXT_ALIGN = { - LEFT = 1, - CENTER = 2, - RIGHT = 3 -} +core.TEXT_ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 } ---@class graphics_border ---@field width integer @@ -73,15 +69,9 @@ end function core.cpair(a, b) return { -- color pairs - color_a = a, - color_b = b, - blit_a = colors.toBlit(a), - blit_b = colors.toBlit(b), + color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b), -- aliases - fgd = a, - bkg = b, - blit_fgd = colors.toBlit(a), - blit_bkg = colors.toBlit(b) + fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b) } end diff --git a/graphics/element.lua b/graphics/element.lua index 05fbb74..4399ac1 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -19,6 +19,7 @@ local element = {} ---@field gframe? graphics_frame frame instead of x/y/width/height ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw +---@field can_focus? boolean true if this element can be focused, false by default ---@alias graphics_args graphics_args_generic ---|waiting_args @@ -32,6 +33,7 @@ local element = {} ---|spinbox_args ---|switch_button_args ---|tabbar_args +---|number_field_args ---|alarm_indicator_light ---|core_map_args ---|data_indicator_args @@ -69,6 +71,7 @@ local element = {} function element.new(args, child_offset_x, child_offset_y) local self = { id = nil, ---@type element_id|nil + is_root = args.parent == nil, elem_type = debug.getinfo(2).name, define_completed = false, p_window = nil, ---@type table @@ -78,6 +81,7 @@ function element.new(args, child_offset_x, child_offset_y) next_id = 0, -- next child ID subscriptions = {}, button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) }, + focused = false, mt = {} } @@ -89,7 +93,8 @@ function element.new(args, child_offset_x, child_offset_y) content_window = nil, ---@type table|nil fg_bg = core.cpair(colors.white, colors.black), frame = core.gframe(1, 1, 1, 1), - children = {} + children = {}, + child_id_map = {} } local name_brief = "graphics.element{" .. self.elem_type .. "}: " @@ -104,6 +109,69 @@ function element.new(args, child_offset_x, child_offset_y) setmetatable(public, self.mt) + ----------------------- + -- PRIVATE FUNCTIONS -- + ----------------------- + + -- use tab to jump to the next focusable field + ---@param reverse boolean + local function _tab_focusable(reverse) + local first_f = nil ---@type graphics_element|nil + local prev_f = nil ---@type graphics_element|nil + local cur_f = nil ---@type graphics_element|nil + local done = false + + ---@param elem graphics_element + local function handle_element(elem) + if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then + if first_f == nil then first_f = elem end + + if cur_f == nil then + if elem.is_focused() then + cur_f = elem + if (not done) and (reverse and prev_f ~= nil) then + cur_f.unfocus() + prev_f.focus() + done = true + end + end + else + if elem.is_focused() then + elem.unfocus() + elseif not (reverse or done) then + cur_f.unfocus() + elem.focus() + done = true + end + end + + prev_f = elem + end + end + + ---@param children table + local function traverse(children) + for i = 1, #children do + local child = children[i] ---@type graphics_base + handle_element(child.get()) + if child.get().is_visible() then traverse(child.children) end + end + end + + traverse(protected.children) + + -- if no element was focused, wrap focus + if first_f ~= nil and not done then + if reverse then + if cur_f ~= nil then cur_f.unfocus() end + if prev_f ~= nil then prev_f.focus() end + else + if cur_f ~= nil then cur_f.unfocus() end + first_f.focus() + end + end + end + ------------------------- -- PROTECTED FUNCTIONS -- ------------------------- @@ -214,85 +282,6 @@ function element.new(args, child_offset_x, child_offset_y) return in_x and in_y end --- luacheck: push ignore ----@diagnostic disable: unused-local, unused-vararg - - -- handle a child element having been added - ---@param id element_id element identifier - ---@param child graphics_element child element - function protected.on_added(id, child) - end - - -- handle a child element having been removed - ---@param id element_id element identifier - function protected.on_removed(id) - end - - -- handle a mouse event - ---@param event mouse_interaction mouse interaction event - function protected.handle_mouse(event) - end - - -- handle data value changes - ---@vararg any value(s) - function protected.on_update(...) - end - - -- callback on control press responses - ---@param result any - function protected.response_callback(result) - end - - -- get value - ---@nodiscard - function protected.get_value() - return protected.value - end - - -- set value - ---@param value any value to set - function protected.set_value(value) - end - - -- set minimum input value - ---@param min integer minimum allowed value - function protected.set_min(min) - end - - -- set maximum input value - ---@param max integer maximum allowed value - function protected.set_max(max) - end - - -- enable the control - function protected.enable() - end - - -- disable the control - function protected.disable() - end - - -- custom recolor command, varies by element if implemented - ---@vararg cpair|color color(s) - function protected.recolor(...) - end - - -- custom resize command, varies by element if implemented - ---@vararg integer sizing - function protected.resize(...) - end - --- luacheck: pop ----@diagnostic enable: unused-local, unused-vararg - - -- start animations - function protected.start_anim() - end - - -- stop animations - function protected.stop_anim() - end - -- get public interface ---@nodiscard ---@return graphics_element element, element_id id @@ -306,6 +295,98 @@ function element.new(args, child_offset_x, child_offset_y) return public, self.id end + -- protected version of public is_focused() + ---@nodiscard + ---@return boolean is_focused + function protected.is_focused() return self.focused end + + -- defocus this element + function protected.defocus() public.unfocus_all() end + + -- request focus management to focus this element + function protected.req_focus() args.parent.__focus_child(public) end + + -- action handlers -- + +-- luacheck: push ignore +---@diagnostic disable: unused-local, unused-vararg + + -- handle a child element having been added + ---@param id element_id element identifier + ---@param child graphics_element child element + function protected.on_added(id, child) end + + -- handle a child element having been removed + ---@param id element_id element identifier + function protected.on_removed(id) end + + -- handle this element having been focused + function protected.on_focused() end + + -- handle this element having been unfocused + function protected.on_unfocused() end + + -- handle this element having been shown + function protected.on_shown() end + + -- handle this element having been hidden + function protected.on_hidden() end + + -- handle a mouse event + ---@param event mouse_interaction mouse interaction event + function protected.handle_mouse(event) end + + -- handle a keyboard event + ---@param event key_interaction key interaction event + function protected.handle_key(event) end + + -- handle data value changes + ---@vararg any value(s) + function protected.on_update(...) end + + -- callback on control press responses + ---@param result any + function protected.response_callback(result) end + + -- get value + ---@nodiscard + function protected.get_value() return protected.value end + + -- set value + ---@param value any value to set + function protected.set_value(value) end + + -- set minimum input value + ---@param min integer minimum allowed value + function protected.set_min(min) end + + -- set maximum input value + ---@param max integer maximum allowed value + function protected.set_max(max) end + + -- enable the control + function protected.enable() end + + -- disable the control + function protected.disable() end + + -- custom recolor command, varies by element if implemented + ---@vararg cpair|color color(s) + function protected.recolor(...) end + + -- custom resize command, varies by element if implemented + ---@vararg integer sizing + function protected.resize(...) end + +-- luacheck: pop +---@diagnostic enable: unused-local, unused-vararg + + -- start animations + function protected.start_anim() end + + -- stop animations + function protected.stop_anim() end + ----------- -- SETUP -- ----------- @@ -324,7 +405,7 @@ function element.new(args, child_offset_x, child_offset_y) self.id = args.id or "__ROOT__" protected.prepare_template(0, 0, 1) else - self.id = args.parent.__add_child(args.id, protected) + self.id, self.ordinal = args.parent.__add_child(args.id, protected) end ---------------------- @@ -358,7 +439,7 @@ function element.new(args, child_offset_x, child_offset_y) -- delete all children for k, v in pairs(protected.children) do - v.delete() + v.get().delete() protected.children[k] = nil end @@ -380,47 +461,59 @@ function element.new(args, child_offset_x, child_offset_y) self.next_y = child.frame.y + child.frame.h - local child_element = child.get() - local id = key ---@type string|integer|nil if id == nil then id = self.next_id self.next_id = self.next_id + 1 end - protected.children[id] = child_element + table.insert(protected.children, child) + + protected.child_id_map[id] = #protected.children + return id end -- remove a child element - ---@param key element_id id - function public.__remove_child(key) - if protected.children[key] ~= nil then - protected.on_removed(key) - protected.children[key] = nil + ---@param id element_id id + function public.__remove_child(id) + local index = protected.child_id_map[id] + if protected.children[index] ~= nil then + protected.on_removed(id) + protected.children[index] = nil + protected.child_id_map[id] = nil end end -- actions to take upon a child element becoming ready (initial draw/construction completed) ---@param key element_id id ---@param child graphics_element - function public.__child_ready(key, child) - protected.on_added(key, child) + function public.__child_ready(key, child) protected.on_added(key, child) end + + -- focus solely on this child + ---@param child graphics_element + function public.__focus_child(child) + if self.is_root then + public.unfocus_all() + child.focus() + else args.parent.__focus_child(child) end end -- get a child element ---@nodiscard ---@param id element_id ---@return graphics_element - function public.get_child(id) return protected.children[id] end + function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end -- remove a child element ---@param id element_id function public.remove(id) - if protected.children[id] ~= nil then - protected.children[id].delete() + local index = protected.child_id_map[id] + if protected.children[index] ~= nil then + protected.children[index].get().delete() protected.on_removed(id) - protected.children[id] = nil + protected.children[index] = nil + protected.child_id_map[id] = nil end end @@ -429,16 +522,13 @@ function element.new(args, child_offset_x, child_offset_y) ---@param id element_id ---@return graphics_element|nil element function public.get_element_by_id(id) - if protected.children[id] == nil then + local index = protected.child_id_map[id] + if protected.children[index] == nil then for _, child in pairs(protected.children) do - local elem = child.get_element_by_id(id) + local elem = child.get().get_element_by_id(id) if elem ~= nil then return elem end end - else - return protected.children[id] - end - - return nil + else return protected.children[index].get() end end -- AUTO-PLACEMENT -- @@ -450,97 +540,113 @@ function element.new(args, child_offset_x, child_offset_y) -- PROPERTIES -- - -- get the foreground/background colors + -- get element id ---@nodiscard - ---@return cpair fg_bg - function public.get_fg_bg() - return protected.fg_bg - end + ---@return element_id + function public.get_id() return self.id end -- get element x ---@nodiscard ---@return integer x - function public.get_x() - return protected.frame.x - end + function public.get_x() return protected.frame.x end -- get element y ---@nodiscard ---@return integer y - function public.get_y() - return protected.frame.y - end + function public.get_y() return protected.frame.y end -- get element width ---@nodiscard ---@return integer width - function public.get_width() - return protected.frame.w - end + function public.get_width() return protected.frame.w end -- get element height ---@nodiscard ---@return integer height - function public.get_height() - return protected.frame.h - end + function public.get_height() return protected.frame.h end + + -- get the foreground/background colors + ---@nodiscard + ---@return cpair fg_bg + function public.get_fg_bg() return protected.fg_bg end -- get the element value ---@nodiscard ---@return any value - function public.get_value() - return protected.get_value() - end + function public.get_value() return protected.get_value() end -- set the element value ---@param value any new value - function public.set_value(value) - protected.set_value(value) - end + function public.set_value(value) protected.set_value(value) end -- set minimum input value ---@param min integer minimum allowed value - function public.set_min(min) - protected.set_min(min) - end + function public.set_min(min) protected.set_min(min) end -- set maximum input value ---@param max integer maximum allowed value - function public.set_max(max) - protected.set_max(max) - end + function public.set_max(max) protected.set_max(max) end + + -- check if this element is enabled + function public.is_enabled() return protected.enabled end -- enable the element function public.enable() - protected.enabled = true - protected.enable() + if not protected.enabled then + protected.enabled = true + protected.enable() + end end -- disable the element function public.disable() - protected.enabled = false - protected.disable() + if protected.enabled then + protected.enabled = false + protected.disable() + end + end + + -- can this element be focused + function public.is_focusable() return args.can_focus end + + -- is this element focused + function public.is_focused() return self.focused end + + -- focus the element + function public.focus() + if args.can_focus and not self.focused then + self.focused = true + protected.on_focused() + end + end + + -- unfocus this element + function public.unfocus() + if args.can_focus and self.focused then + self.focused = false + protected.on_unfocused() + end + end + + -- unfocus this element and all its children + function public.unfocus_all() + public.unfocus() + for _, child in pairs(protected.children) do child.get().unfocus() end end -- custom recolor command, varies by element if implemented ---@vararg cpair|color color(s) - function public.recolor(...) - protected.recolor(...) - end + function public.recolor(...) protected.recolor(...) end -- resize attributes of the element value if supported ---@vararg number dimensions (element specific) - function public.resize(...) - protected.resize(...) - end + function public.resize(...) protected.resize(...) end -- reposition the element window
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner ---@param x integer x position relative to parent frame ---@param y integer y position relative to parent frame - function public.reposition(x, y) - protected.window.reposition(x, y) - end + function public.reposition(x, y) protected.window.reposition(x, y) end -- FUNCTION CALLBACKS -- @@ -553,12 +659,12 @@ function element.new(args, child_offset_x, child_offset_y) local ini_in = protected.in_window_bounds(x_ini, y_ini) if ini_in then - if event.type == events.CLICK_TYPE.UP or event.type == events.CLICK_TYPE.DRAG then + if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then -- make sure we don't handle mouse events that started before this element was made visible if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then return end - elseif event.type == events.CLICK_TYPE.DOWN then + elseif event.type == events.MOUSE_CLICK.DOWN then self.button_down[event.button] = event.initial end @@ -566,25 +672,39 @@ function element.new(args, child_offset_x, child_offset_y) -- handle the mouse event then pass to children protected.handle_mouse(event_T) - for _, child in pairs(protected.children) do child.handle_mouse(event_T) end + for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end + elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then + -- clicked out, unfocus this element and children + public.unfocus_all() end - elseif event.type == events.CLICK_TYPE.DOWN then - -- don't track this click + else + -- don't track clicks while hidden self.button_down[event.button] = events.new_coord_2d(-1, -1) end end + -- handle a keyboard click if this element is visible and focused + ---@param event key_interaction keyboard interaction event + function public.handle_key(event) + if protected.window.isVisible() then + if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then + -- try to jump to the next/previous focusable field + _tab_focusable(event.shift) + else + -- handle the key event then pass to children + if self.focused then protected.handle_key(event) end + for _, child in pairs(protected.children) do child.get().handle_key(event) end + end + end + end + -- draw the element given new data ---@vararg any new data - function public.update(...) - protected.on_update(...) - end + function public.update(...) protected.on_update(...) end -- on a control request response ---@param result any - function public.on_response(result) - protected.response_callback(result) - end + function public.on_response(result) protected.response_callback(result) end -- register a callback with a PSIL, allowing for automatic unregister on delete
-- do not use graphics elements directly with PSIL subscribe() @@ -598,6 +718,9 @@ function element.new(args, child_offset_x, child_offset_y) -- VISIBILITY & ANIMATIONS -- + -- check if this element is visible + function public.is_visible() return protected.window.isVisible() end + -- show the element and enables animations by default ---@param animate? boolean true (default) to automatically resume animations function public.show(animate) @@ -610,44 +733,39 @@ function element.new(args, child_offset_x, child_offset_y) ---@see graphics_element.content_redraw function public.hide() public.freeze_all() -- stop animations for efficiency/performance + public.unfocus_all() protected.window.setVisible(false) end -- start/resume animation(s) - function public.animate() - protected.start_anim() - end + function public.animate() protected.start_anim() end -- start/resume animation(s) for this element and all its children
-- only animates if a window is visible function public.animate_all() if protected.window.isVisible() then public.animate() - for _, child in pairs(protected.children) do child.animate_all() end + for _, child in pairs(protected.children) do child.get().animate_all() end end end -- freeze animation(s) - function public.freeze() - protected.stop_anim() - end + function public.freeze() protected.stop_anim() end -- freeze animation(s) for this element and all its children function public.freeze_all() public.freeze() - for _, child in pairs(protected.children) do child.freeze_all() end + for _, child in pairs(protected.children) do child.get().freeze_all() end end -- re-draw the element - function public.redraw() - protected.window.redraw() - end + function public.redraw() protected.window.redraw() end -- if a content window is set, clears it then re-draws all children function public.content_redraw() if protected.content_window ~= nil then protected.content_window.clear() - for _, child in pairs(protected.children) do child.redraw() end + for _, child in pairs(protected.children) do child.get().redraw() end end end diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua index 574be66..a0d5949 100644 --- a/graphics/elements/controls/app.lua +++ b/graphics/elements/controls/app.lua @@ -5,7 +5,7 @@ local tcd = require("scada-common.tcd") local core = require("graphics.core") local element = require("graphics.element") -local CLICK_TYPE = core.events.CLICK_TYPE +local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class app_button_args ---@field text string app icon text @@ -98,14 +98,14 @@ local function app_button(args) ---@param event mouse_interaction mouse event function e.handle_mouse(event) if e.enabled then - if event.type == CLICK_TYPE.TAP then + if event.type == MOUSE_CLICK.TAP then show_pressed() -- show as unpressed in 0.25 seconds if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end args.callback() - elseif event.type == CLICK_TYPE.DOWN then + elseif event.type == MOUSE_CLICK.DOWN then show_pressed() - elseif event.type == CLICK_TYPE.UP then + elseif event.type == MOUSE_CLICK.UP then show_unpressed() if e.in_frame_bounds(event.current.x, event.current.y) then args.callback() @@ -117,7 +117,7 @@ local function app_button(args) -- set the value (true simulates pressing the app button) ---@param val boolean new value function e.set_value(val) - if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end + if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end end -- initial draw diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index ac1b23d..f6ac2d3 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -174,7 +174,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_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end + if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end end -- show the button as disabled diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index 36e1914..da555be 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -5,7 +5,8 @@ local tcd = require("scada-common.tcd") local core = require("graphics.core") local element = require("graphics.element") -local CLICK_TYPE = core.events.CLICK_TYPE +local MOUSE_CLICK = core.events.MOUSE_CLICK +local KEY_CLICK = core.events.KEY_CLICK ---@class push_button_args ---@field text string button text @@ -32,7 +33,8 @@ local function push_button(args) local text_width = string.len(args.text) - -- single line height, calculate width + -- set automatic settings + args.can_focus = true args.height = 1 args.min_width = args.min_width or 0 args.width = math.max(text_width, args.min_width) @@ -76,14 +78,14 @@ local function push_button(args) ---@param event mouse_interaction mouse event function e.handle_mouse(event) if e.enabled then - if event.type == CLICK_TYPE.TAP then + if event.type == MOUSE_CLICK.TAP then show_pressed() -- show as unpressed in 0.25 seconds if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end args.callback() - elseif event.type == CLICK_TYPE.DOWN then + elseif event.type == MOUSE_CLICK.DOWN then show_pressed() - elseif event.type == CLICK_TYPE.UP then + elseif event.type == MOUSE_CLICK.UP then show_unpressed() if e.in_frame_bounds(event.current.x, event.current.y) then args.callback() @@ -92,10 +94,21 @@ local function push_button(args) end end + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == KEY_CLICK.DOWN then + if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then + args.callback() + e.defocus() + end + end + end + -- set the value (true simulates pressing the button) ---@param val boolean new value function e.set_value(val) - if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end + if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end end -- show butten as enabled @@ -118,6 +131,9 @@ local function push_button(args) end end + e.on_focused = show_pressed + e.on_unfocused = show_unpressed + -- initial draw draw() diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua index 9185430..646c724 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/sidebar.lua @@ -5,7 +5,7 @@ local tcd = require("scada-common.tcd") local core = require("graphics.core") local element = require("graphics.element") -local CLICK_TYPE = core.events.CLICK_TYPE +local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class sidebar_tab ---@field char string character identifier @@ -85,22 +85,22 @@ local function sidebar(args) local ini_idx = math.ceil(event.initial.y / 3) if args.tabs[cur_idx] ~= nil then - if event.type == CLICK_TYPE.TAP then + if event.type == MOUSE_CLICK.TAP then e.value = cur_idx draw(true) -- show as unpressed in 0.25 seconds tcd.dispatch(0.25, function () draw(false) end) args.callback(e.value) - elseif event.type == CLICK_TYPE.DOWN then + elseif event.type == MOUSE_CLICK.DOWN then draw(true, cur_idx) - elseif event.type == CLICK_TYPE.UP then + elseif event.type == MOUSE_CLICK.UP then if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then e.value = cur_idx draw(false) args.callback(e.value) else draw(false) end end - elseif event.type == CLICK_TYPE.UP then + elseif event.type == MOUSE_CLICK.UP then draw(false) end end diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua new file mode 100644 index 0000000..a32d43f --- /dev/null +++ b/graphics/elements/form/number_field.lua @@ -0,0 +1,136 @@ +-- Numeric Value Entry Graphics Element + +local util = require("scada-common.util") + +local core = require("graphics.core") +local element = require("graphics.element") + +local KEY_CLICK = core.events.KEY_CLICK + +---@class number_field_args +---@field default? number default value, defaults to 0 +---@field min? number minimum, forced on unfocus +---@field max? number maximum, forced on unfocus +---@field max_digits? integer maximum number of digits, defaults to width +---@field allow_decimal? boolean true to allow decimals +---@field allow_negative? boolean true to allow negative numbers +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer auto incremented if omitted +---@field width? integer parent width if omitted +---@field fg_bg? cpair foreground/background colors +---@field hidden? boolean true to hide on initial draw + +-- new numeric form field +---@param args number_field_args +---@return graphics_element element, element_id id +local function number_field(args) + args.height = 1 + args.can_focus = true + + -- create new graphics element base object + local e = element.new(args) + + local has_decimal = false + + args.max_digits = args.max_digits or e.frame.w + + -- set initial value + e.value = "" .. (args.default or 0) + + local function show() + -- clear and print + e.w_set_cur(1, 1) + e.w_write(string.rep(" ", e.frame.w)) + e.w_set_cur(1, 1) + e.w_set_fgd(colors.black) + e.w_write(e.value) + if e.is_focused() then + e.w_set_fgd(colors.lightGray) + e.w_write("_") + end + end + + -- 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 core.events.was_clicked(event.type) then + e.req_focus() + end + end + + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then + if tonumber(event.name) then + e.value = util.trinary(e.value == "0", "", e.value) .. tonumber(event.name) + end + + show() + elseif event.type == KEY_CLICK.DOWN then + if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then + e.value = string.sub(e.value, 1, string.len(e.value) - 1) + has_decimal = string.find(e.value, "%.") ~= nil + show() + elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then + e.value = e.value .. "." + has_decimal = true + show() + elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then + e.value = "-" + show() + end + end + end + + -- set the value + ---@param val number number to show + function e.set_value(val) e.value = val end + + -- set minimum input value + ---@param min integer minimum allowed value + function e.set_min(min) args.min = min end + + -- set maximum input value + ---@param max integer maximum allowed value + function e.set_max(max) args.max = max end + + -- handle focus change + e.on_focused = show + + function e.on_unfocused() + local val = tonumber(e.value) + local max = tonumber(args.max) + local min = tonumber(args.min) + + if type(val) == "number" then + if type(args.max) == "number" and val > max then + e.value = "" .. max + elseif type(args.min) == "number" and val < min then + e.value = "" .. min + end + else + e.value = "" + end + + show() + end + + -- enable this input + function e.enable() + end + + -- disable this input + function e.disable() + end + + -- initial draw + show() + + return e.complete() +end + +return number_field diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index 1bc3556..0268951 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/listbox.lua @@ -5,7 +5,7 @@ local tcd = require("scada-common.tcd") local core = require("graphics.core") local element = require("graphics.element") -local CLICK_TYPE = core.events.CLICK_TYPE +local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class listbox_args ---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled) @@ -223,7 +223,7 @@ local function listbox(args) ---@param event mouse_interaction mouse event function e.handle_mouse(event) if e.enabled then - if event.type == CLICK_TYPE.TAP then + if event.type == MOUSE_CLICK.TAP then if event.current.x == e.frame.w then if event.current.y == 1 or event.current.y < bar_bounds[1] then draw_arrows(1) @@ -235,7 +235,7 @@ local function listbox(args) if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end end end - elseif event.type == CLICK_TYPE.DOWN then + elseif event.type == MOUSE_CLICK.DOWN then if event.current.x == e.frame.w then if event.current.y == 1 or event.current.y < bar_bounds[1] then draw_arrows(1) @@ -250,10 +250,10 @@ local function listbox(args) mouse_last_y = event.current.y end end - elseif event.type == CLICK_TYPE.UP then + elseif event.type == MOUSE_CLICK.UP then holding_bar = false draw_arrows(0) - elseif event.type == CLICK_TYPE.DRAG then + elseif event.type == MOUSE_CLICK.DRAG then if holding_bar then -- if mouse is within vertical frame, including the grip point if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then @@ -266,9 +266,9 @@ local function listbox(args) mouse_last_y = event.current.y end end - elseif event.type == CLICK_TYPE.SCROLL_DOWN then + elseif event.type == MOUSE_CLICK.SCROLL_DOWN then scroll_down() - elseif event.type == CLICK_TYPE.SCROLL_UP then + elseif event.type == MOUSE_CLICK.SCROLL_UP then scroll_up() end end diff --git a/graphics/events.lua b/graphics/events.lua index ca56588..df5f7eb 100644 --- a/graphics/events.lua +++ b/graphics/events.lua @@ -14,8 +14,8 @@ events.CLICK_BUTTON = { MID_BUTTON = 3 } ----@enum CLICK_TYPE -events.CLICK_TYPE = { +---@enum MOUSE_CLICK +local MOUSE_CLICK = { TAP = 1, -- screen tap (complete click) DOWN = 2, -- button down UP = 3, -- button up (completed a click) @@ -24,6 +24,18 @@ events.CLICK_TYPE = { SCROLL_UP = 6 -- scroll up } +events.MOUSE_CLICK = MOUSE_CLICK + +---@enum KEY_CLICK +local KEY_CLICK = { + DOWN = 1, + HELD = 2, + UP = 3, + CHAR = 4 +} + +events.KEY_CLICK = KEY_CLICK + -- create a new 2D coordinate ---@param x integer ---@param y integer @@ -35,13 +47,25 @@ events.new_coord_2d = _coord2d ---@class mouse_interaction ---@field monitor string ---@field button CLICK_BUTTON ----@field type CLICK_TYPE +---@field type MOUSE_CLICK ---@field initial coordinate_2d ---@field current coordinate_2d +---@class key_interaction +---@field type KEY_CLICK +---@field key number key code +---@field name string key character name +---@field shift boolean shift held +---@field ctrl boolean ctrl held +---@field alt boolean alt held + local handler = { -- left, right, middle button down tracking - button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) } + button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) }, + -- keyboard modifiers + shift = false, + alt = false, + ctrl = false } -- create a new monitor touch mouse interaction event @@ -54,7 +78,7 @@ local function _monitor_touch(monitor, x, y) return { monitor = monitor, button = events.CLICK_BUTTON.GENERIC, - type = events.CLICK_TYPE.TAP, + type = MOUSE_CLICK.TAP, initial = _coord2d(x, y), current = _coord2d(x, y) } @@ -63,7 +87,7 @@ end -- create a new mouse button mouse interaction event ---@nodiscard ---@param button CLICK_BUTTON mouse button ----@param type CLICK_TYPE click type +---@param type MOUSE_CLICK click type ---@param x1 integer initial x ---@param y1 integer initial y ---@param x2 integer current x @@ -81,7 +105,7 @@ end -- create a new generic mouse interaction event ---@nodiscard ----@param type CLICK_TYPE +---@param type MOUSE_CLICK ---@param x integer ---@param y integer ---@return mouse_interaction @@ -113,8 +137,8 @@ end -- check if an event qualifies as a click (tap or up) ---@nodiscard ----@param t CLICK_TYPE -function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end +---@param t MOUSE_CLICK +function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end -- create a new mouse event to pass onto graphics renderer
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch @@ -127,32 +151,65 @@ function events.new_mouse_event(event_type, opt, x, y) if event_type == "mouse_click" then ---@cast opt 1|2|3 handler.button_down[opt] = _coord2d(x, y) - return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y) + return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y) elseif event_type == "mouse_up" then ---@cast opt 1|2|3 local initial = handler.button_down[opt] ---@type coordinate_2d - return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y) + return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y) elseif event_type == "monitor_touch" then ---@cast opt string return _monitor_touch(opt, x, y) elseif event_type == "mouse_drag" then ---@cast opt 1|2|3 local initial = handler.button_down[opt] ---@type coordinate_2d - return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y) + return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y) elseif event_type == "mouse_scroll" then ---@cast opt 1|-1 - local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP) + local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP) return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y) end end --- create a new key event to pass onto graphics renderer
+-- create a new keyboard interaction event +---@nodiscard +---@param click_type KEY_CLICK key click type +---@param key integer|string keyboard key code or character for 'char' event +---@return key_interaction +local function _key_event(click_type, key) + local name = key + if type(key) == "number" then name = keys.getName(key) end + return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt } +end + +-- create a new keyboard event to pass onto graphics renderer
-- supports: char, key, and key_up ----@param event_type os_event -function events.new_key_event(event_type) +---@param event_type os_event OS event to handle +---@param key integer keyboard key code +---@param held boolean? if the key is being held (for 'key' event) +---@return key_interaction|nil +function events.new_key_event(event_type, key, held) if event_type == "char" then + return _key_event(KEY_CLICK.CHAR, key) elseif event_type == "key" then + if key == keys.leftShift or key == keys.rightShift then + handler.shift = true + elseif key == keys.leftCtrl or key == keys.rightCtrl then + handler.ctrl = true + elseif key == keys.leftAlt or key == keys.rightAlt then + handler.alt = true + else + return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key) + end elseif event_type == "key_up" then + if key == keys.leftShift or key == keys.rightShift then + handler.shift = false + elseif key == keys.leftCtrl or key == keys.rightCtrl then + handler.ctrl = false + elseif key == keys.leftAlt or key == keys.rightAlt then + handler.alt = false + else + return _key_event(KEY_CLICK.UP, key) + end end end From b173b72f2135916d722a75472f2b9024351b540a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 17 Sep 2023 00:12:14 -0400 Subject: [PATCH 04/35] #344 numeric field cleanup --- graphics/elements/form/number_field.lua | 35 +++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index a32d43f..da0a7ae 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -14,6 +14,7 @@ local KEY_CLICK = core.events.KEY_CLICK ---@field max_digits? integer maximum number of digits, defaults to width ---@field allow_decimal? boolean true to allow decimals ---@field allow_negative? boolean true to allow negative numbers +---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -22,7 +23,7 @@ local KEY_CLICK = core.events.KEY_CLICK ---@field fg_bg? cpair foreground/background colors ---@field hidden? boolean true to hide on initial draw --- new numeric form field +-- new numeric entry field ---@param args number_field_args ---@return graphics_element element, element_id id local function number_field(args) @@ -37,16 +38,26 @@ local function number_field(args) args.max_digits = args.max_digits or e.frame.w -- set initial value - e.value = "" .. (args.default or 0) + e.value = util.strval(args.default or 0) + -- draw input local function show() + if e.enabled then + e.w_set_bkg(args.fg_bg.bkg) + e.w_set_fgd(args.fg_bg.fgd) + else + e.w_set_bkg(args.dis_fg_bg.bkg) + e.w_set_fgd(args.dis_fg_bg.fgd) + end + -- clear and print e.w_set_cur(1, 1) e.w_write(string.rep(" ", e.frame.w)) e.w_set_cur(1, 1) - e.w_set_fgd(colors.black) e.w_write(e.value) - if e.is_focused() then + + -- show cursor if focused + if e.is_focused() and e.enabled then e.w_set_fgd(colors.lightGray) e.w_write("_") end @@ -67,9 +78,8 @@ local function number_field(args) if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then if tonumber(event.name) then e.value = util.trinary(e.value == "0", "", e.value) .. tonumber(event.name) + show() end - - show() elseif event.type == KEY_CLICK.DOWN then if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then e.value = string.sub(e.value, 1, string.len(e.value) - 1) @@ -98,9 +108,10 @@ local function number_field(args) ---@param max integer maximum allowed value function e.set_max(max) args.max = max end - -- handle focus change + -- handle focused e.on_focused = show + -- handle unfocused function e.on_unfocused() local val = tonumber(e.value) local max = tonumber(args.max) @@ -119,13 +130,9 @@ local function number_field(args) show() end - -- enable this input - function e.enable() - end - - -- disable this input - function e.disable() - end + -- on enable/disable + e.enable = show + e.disable = show -- initial draw show() From 7a87499aa47a77eaade542b55274b4c8728cb29b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 17 Sep 2023 16:37:11 -0400 Subject: [PATCH 05/35] #344 radio button appearance changes --- coordinator/startup.lua | 2 +- coordinator/ui/components/process_ctl.lua | 4 ++-- coordinator/ui/components/unit_detail.lua | 2 +- graphics/elements/controls/radio_button.lua | 25 ++++++++++----------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 83d31c1..4d06b3d 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.0.12" +local COORDINATOR_VERSION = "v1.0.13" local println = util.println local println_ts = util.println_ts diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index 4c69b87..d1ca1b0 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -241,7 +241,7 @@ local function new_view(root, x, y) ------------------------- local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" } - local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.purple} + local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple} mode.register(facility.ps, "process_mode", mode.set_value) @@ -335,7 +335,7 @@ local function new_view(root, x, y) status.register(facility.ps, "current_waste_product", status.update) - local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown} + local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown} local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)} waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index fe98727..fb1eccd 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -476,7 +476,7 @@ local function init(parent, id) local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" } - local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray} + local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple} group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end) diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 8c2d3ec..33a65d5 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -1,13 +1,15 @@ -- Radio Button Graphics Element +local util = require("scada-common.util") + local core = require("graphics.core") local element = require("graphics.element") ---@class radio_button_args ---@field options table button options ---@field callback function function to call on touch ----@field radio_colors cpair colors for radio button center dot when active (a) or inactive (b) ----@field radio_bg color background color of radio button +---@field radio_colors cpair radio button colors (inner & outer) +---@field select_color color color for radio button border when selected ---@field default? integer default state, defaults to options[1] ---@field min_width? integer text length + 2 if omitted ---@field parent graphics_element @@ -24,6 +26,8 @@ local function radio_button(args) assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field") assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required") assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field") + assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_button: radio_colors is a required field") + assert(type(args.select_color) == "number", "graphics.elements.controls.radio_button: select_color is a required field") assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "graphics.elements.controls.radio_button: default must be nil or a number > 0") assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), @@ -56,21 +60,16 @@ local function radio_button(args) for i = 1, #args.options do local opt = args.options[i] ---@type string + local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a) + local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b) + e.w_set_cur(1, i) - if e.value == i then - -- show as selected - e.w_set_fgd(args.radio_colors.color_a) - e.w_set_bkg(args.radio_bg) - else - -- show as unselected - e.w_set_fgd(args.radio_colors.color_b) - e.w_set_bkg(args.radio_bg) - end - + e.w_set_fgd(inner_color) + e.w_set_bkg(outer_color) e.w_write("\x88") - e.w_set_fgd(args.radio_bg) + e.w_set_fgd(outer_color) e.w_set_bkg(e.fg_bg.bkg) e.w_write("\x95") From a2182d9566cf9bd275b933233d100838e1ed9ba8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 19 Sep 2023 23:51:58 -0400 Subject: [PATCH 06/35] #344 work in progress on text field & paste events, re-show number field on val/min/max changes --- graphics/element.lua | 18 ++- graphics/elements/controls/push_button.lua | 1 + graphics/elements/form/number_field.lua | 15 +- graphics/elements/form/text_field.lua | 177 +++++++++++++++++++++ 4 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 graphics/elements/form/text_field.lua diff --git a/graphics/element.lua b/graphics/element.lua index 4399ac1..f356c03 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -34,6 +34,7 @@ local element = {} ---|switch_button_args ---|tabbar_args ---|number_field_args +---|text_field_args ---|alarm_indicator_light ---|core_map_args ---|data_indicator_args @@ -340,6 +341,10 @@ function element.new(args, child_offset_x, child_offset_y) ---@param event key_interaction key interaction event function protected.handle_key(event) end + -- handle a paste event + ---@param text string pasted text + function protected.handle_paste(text) end + -- handle data value changes ---@vararg any value(s) function protected.on_update(...) end @@ -603,6 +608,7 @@ function element.new(args, child_offset_x, child_offset_y) if protected.enabled then protected.enabled = false protected.disable() + public.unfocus_all() end end @@ -614,7 +620,7 @@ function element.new(args, child_offset_x, child_offset_y) -- focus the element function public.focus() - if args.can_focus and not self.focused then + if args.can_focus and protected.enabled and not self.focused then self.focused = true protected.on_focused() end @@ -698,6 +704,16 @@ function element.new(args, child_offset_x, child_offset_y) end end + -- handle text paste + ---@param text string pasted text + function public.handle_paste(text) + if protected.window.isVisible() then + -- handle the paste event then pass to children + if self.focused then protected.handle_paste(text) end + for _, child in pairs(protected.children) do child.get().handle_paste(text) end + end + end + -- draw the element given new data ---@vararg any new data function public.update(...) protected.on_update(...) end diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index da555be..b8e3013 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -131,6 +131,7 @@ local function push_button(args) end end + -- handle focus change e.on_focused = show_pressed e.on_unfocused = show_unpressed diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index da0a7ae..e0e2426 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -98,15 +98,24 @@ local function number_field(args) -- set the value ---@param val number number to show - function e.set_value(val) e.value = val end + function e.set_value(val) + e.value = val + show() + end -- set minimum input value ---@param min integer minimum allowed value - function e.set_min(min) args.min = min end + function e.set_min(min) + args.min = min + show() + end -- set maximum input value ---@param max integer maximum allowed value - function e.set_max(max) args.max = max end + function e.set_max(max) + args.max = max + show() + end -- handle focused e.on_focused = show diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua new file mode 100644 index 0000000..47d512b --- /dev/null +++ b/graphics/elements/form/text_field.lua @@ -0,0 +1,177 @@ +-- Text Value Entry Graphics Element + +local util = require("scada-common.util") + +local core = require("graphics.core") +local element = require("graphics.element") + +local KEY_CLICK = core.events.KEY_CLICK +local MOUSE_CLICK = core.events.MOUSE_CLICK + +---@class text_field_args +---@field value? string initial value +---@field max_len? integer maximum string length +---@field dis_fg_bg? cpair foreground/background colors when disabled +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer auto incremented if omitted +---@field width? integer parent width if omitted +---@field fg_bg? cpair foreground/background colors +---@field hidden? boolean true to hide on initial draw + +-- new text entry field +---@param args text_field_args +---@return graphics_element element, element_id id +local function text_field(args) + args.height = 1 + args.can_focus = true + + -- create new graphics element base object + local e = element.new(args) + + -- set initial value + e.value = args.value or "" + + local max_len = 1000 -- temporary + + local frame_start = 1 + local visible_text = e.value + local cursor_pos = string.len(visible_text) + 1 + + local function frame__update_visible() + visible_text = string.sub(e.value, frame_start, frame_start + math.min(string.len(e.value), args.width) - 1) + end + + -- draw input + local function show() + frame__update_visible() + + if e.enabled then + e.w_set_bkg(args.fg_bg.bkg) + e.w_set_fgd(args.fg_bg.fgd) + else + e.w_set_bkg(args.dis_fg_bg.bkg) + e.w_set_fgd(args.dis_fg_bg.fgd) + end + + -- clear and print + e.w_set_cur(1, 1) + e.w_write(string.rep(" ", e.frame.w)) + e.w_set_cur(1, 1) + + if e.is_focused() and e.enabled then + -- write text with cursor + if cursor_pos == (string.len(visible_text) + 1) then + -- write text with cursor at the end, no need to blit + e.w_write(visible_text) + e.w_set_fgd(colors.lightGray) + e.w_write("_") + else + local a, b = "", "" + + if cursor_pos <= string.len(visible_text) then + a = args.fg_bg.blit_bkg + b = args.fg_bg.blit_fgd + end + + local b_fgd = string.rep(args.fg_bg.blit_fgd, cursor_pos - 1) .. a .. string.rep(args.fg_bg.blit_fgd, string.len(visible_text) - cursor_pos) + local b_bkg = string.rep(args.fg_bg.blit_bkg, cursor_pos - 1) .. b .. string.rep(args.fg_bg.blit_bkg, string.len(visible_text) - cursor_pos) + + e.w_blit(visible_text, b_fgd, b_bkg) + end + else + -- write text without cursor + e.w_write(visible_text) + end + end + + local function frame__try_lshift() + if frame_start > 1 then + frame_start = frame_start - 1 + show() + end + end + + local function frame__try_rshift() + if (frame_start + args.width - 1) < string.len(e.value) then + frame_start = frame_start + 1 + show() + end + end + + -- 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 core.events.was_clicked(event.type) then + e.req_focus() + + if event.type == MOUSE_CLICK.UP then + cursor_pos = math.min(event.current.x, string.len(visible_text) + 1) + show() + end + end + end + + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == KEY_CLICK.CHAR and string.len(e.value) < max_len then + e.value = string.sub(e.value, 1, frame_start + cursor_pos - 2) .. event.name .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) + frame__update_visible() + if cursor_pos <= string.len(visible_text) then + cursor_pos = cursor_pos + 1 + show() + else frame__try_rshift() end + elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then + if (event.key == keys.backspace or event.key == keys.delete) then + -- remove charcter at cursor + e.value = string.sub(e.value, 1, frame_start + cursor_pos - 3) .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) + if cursor_pos > 1 then + cursor_pos = cursor_pos - 1 + show() + else frame__try_lshift() end + elseif event.key == keys.left then + if cursor_pos > 1 then + cursor_pos = cursor_pos - 1 + show() + else frame__try_lshift() end + elseif event.key == keys.right then + if cursor_pos <= string.len(visible_text) then + cursor_pos = cursor_pos + 1 + show() + else frame__try_rshift() end + end + end + end + + -- set the value + ---@param val string string to show + function e.set_value(val) + e.value = val + frame_start = 1 + math.max(0, string.len(val) - args.width) + frame__update_visible() + cursor_pos = string.len(visible_text) + 1 + show() + end + + function e.handle_paste(text) + e.set_value(text) + end + + -- handle focus + e.on_focused = show + e.on_unfocused = show + + -- on enable/disable + e.enable = show + e.disable = show + + -- initial draw + show() + + return e.complete() +end + +return text_field From 611b048cb491724b734006ff0ee5f70668705655 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 19 Sep 2023 23:52:22 -0400 Subject: [PATCH 07/35] #307 work in progress PLC configurator --- configure.lua | 2 +- reactor-plc/configure.lua | 236 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 reactor-plc/configure.lua diff --git a/configure.lua b/configure.lua index 20e67f3..5234f1d 100644 --- a/configure.lua +++ b/configure.lua @@ -1,7 +1,7 @@ print("CONFIGURE> SCANNING FOR CONFIGURATOR...") if fs.exists("reactor-plc/configure.lua") then - require("reactor-plc/configure.lua").configure() + require("reactor-plc.configure").configure() elseif fs.exists("rtu/startup.lua") then print("CONFIGURE> RTU CONFIGURATOR NOT YET IMPLEMENTED IN BETA") elseif fs.exists("supervisor/startup.lua") then diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua new file mode 100644 index 0000000..3b51c5e --- /dev/null +++ b/reactor-plc/configure.lua @@ -0,0 +1,236 @@ +-- +-- Configuration GUI +-- + +local core = require("graphics.core") +local log = require("scada-common.log") + +log.init("/log.txt", log.MODE.APPEND, true) + +local DisplayBox = require("graphics.elements.displaybox") +local Div = require("graphics.elements.div") +local MultiPane = require("graphics.elements.multipane") +local Rectangle = require("graphics.elements.rectangle") +local TextBox = require("graphics.elements.textbox") + +local RadioButton = require("graphics.elements.controls.radio_button") +local PushButton = require("graphics.elements.controls.push_button") +local CheckBox = require("graphics.elements.controls.checkbox") + +local NumberField = require("graphics.elements.form.number_field") +local TextField = require("graphics.elements.form.text_field") + +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") + +local print = util.print +local println = util.println + +local cpair = core.cpair + +local CENTER = core.TEXT_ALIGN.CENTER + +---@class plc_configurator +local configurator = {} + +local style = {} + +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) +style.label = cpair(colors.gray, colors.lightGray) + +style.colors = { + { c = colors.red, hex = 0xdf4949 }, + { c = colors.orange, hex = 0xffb659 }, + { c = colors.yellow, hex = 0xfffc79 }, + { c = colors.lime, hex = 0x80ff80 }, + { c = colors.green, hex = 0x4aee8a }, + { c = colors.cyan, hex = 0x34bac8 }, + { c = colors.lightBlue, hex = 0x6cc0f2 }, + { c = colors.blue, hex = 0x0096ff }, + { c = colors.purple, hex = 0xb156ee }, + { c = colors.pink, hex = 0xf26ba2 }, + { c = colors.magenta, hex = 0xf9488a }, + -- { c = colors.white, hex = 0xf0f0f0 }, + { c = colors.lightGray, hex = 0xcacaca }, + { c = colors.gray, hex = 0x575757 }, + -- { c = colors.black, hex = 0x191919 }, + -- { c = colors.brown, hex = 0x7f664c } +} + +local tool_ctl = { + networked = false, + + set_networked = nil, ---@type function + next_from_plc = nil, ---@type function + back_from_log = nil ---@type function +} + +local function _config_view(display) + -- window header message + TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,height=1,fg_bg=style.header} + + local root_pane_div = Div{parent=display,x=1,y=2} + + local main_page = Div{parent=root_pane_div,x=1,y=1} + local plc_cfg = Div{parent=root_pane_div,x=1,y=1} + local net_cfg = Div{parent=root_pane_div,x=1,y=1} + local log_cfg = Div{parent=root_pane_div,x=1,y=1} + + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg}} + + -- MAIN PAGE + + TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."} + + PushButton{parent=main_page,x=2,y=5,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=main_page,x=2,y=7,min_width=20,text="View Configuration",callback=function()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=main_page,x=2,y=9,min_width=28,text="Import Legacy 'config.lua'",callback=function()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=cpair(colors.white,colors.gray)} +---@diagnostic disable-next-line: undefined-field + PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=function()os.queueEvent("exit")end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + local nav_fg_bg = cpair(colors.black,colors.white) + local nav_a_fg_bg = cpair(colors.white,colors.gray) + + + -- PLC CONFIG + + local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49} + local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49} + + local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2}} + + TextBox{parent=plc_cfg,x=1,y=2,height=1,text_align=CENTER,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)} + + TextBox{parent=plc_c_1,x=1,y=1,height=1,text_align=CENTER,text="Would you like to set this PLC as networked?"} + TextBox{parent=plc_c_1,x=1,y=3,height=4,text_align=CENTER,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=cpair(colors.gray,colors.lightGray)} + + CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.set_networked(v)end} + + PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + TextBox{parent=plc_c_2,x=1,y=1,height=1,text_align=CENTER,text="Please enter the reactor unit ID for this PLC."} + TextBox{parent=plc_c_2,x=1,y=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)} + + TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,text="Unit #"} + local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=1,min=1,fg_bg=cpair(colors.black,colors.white)} + + PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=function()tool_ctl.next_from_plc()end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + -- NET CONFIG + + local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} + local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} + + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2}} + + TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please set the network channels below."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)} + + TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"} + NumberField{parent=net_c_1,x=1,y=9,width=7,default=16240,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} + TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,text="PLC Channel"} + NumberField{parent=net_c_1,x=1,y=12,width=7,allow_decimal=true,allow_negative=true,default=16241,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} + + PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + TextBox{parent=net_c_2,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} + TextBox{parent=net_c_2,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=cpair(colors.gray,colors.lightGray)} + + TextBox{parent=net_c_2,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"} + TextField{parent=net_c_2,x=1,y=12,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} + + PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + -- LOG CONFIG + + local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} + + TextBox{parent=log_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,text="Log File Mode"} + local mode = RadioButton{parent=log_c_1,x=1,y=4,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + + TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"} + TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value="/log.txt",fg_bg=cpair(colors.black,colors.white)} + + CheckBox{parent=log_c_1,x=1,y=10,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} + TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)} + + PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()tool_ctl.back_from_log()end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + -- overwrite functions now that we have the elements + + function tool_ctl.set_networked(enable) + tool_ctl.networked = enable + if enable then u_id.set_max(4) else u_id.set_max(999) end + end + + function tool_ctl.next_from_plc() + if tool_ctl.networked then main_pane.set_value(3) else main_pane.set_value(4) end + end + + function tool_ctl.back_from_log() + if tool_ctl.networked then main_pane.set_value(3) else main_pane.set_value(2) end + end +end + +function configurator.configure() + -- 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 + + -- init front panel view + local display = DisplayBox{window=term.current(),fg_bg=style.root} + _config_view(display) + + while true do + local event, param1, param2, param3 = util.pull_event() + + -- handle event + if event == "timer" then + -- notify timer callback dispatcher if no other timer case claimed this event + tcd.handle(param1) + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then + -- handle a mouse event + local m_e = core.events.new_mouse_event(event, param1, param2, param3) + if m_e then display.handle_mouse(m_e) end + elseif event == "char" or event == "key" or event == "key_up" then + -- handle a key event + local k_e = core.events.new_key_event(event, param1, param2) + if k_e then display.handle_key(k_e) end + elseif event == "paste" then + -- handle a paste event + display.handle_paste(param1) + elseif event == "exit" then + return + end + + -- check for termination request + if event == "terminate" then + println("terminate requested, exiting config") + return false + end + end +end + +return configurator From d21604ea098ca08428b2a97855e496e3fa58e404 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 00:09:37 -0400 Subject: [PATCH 08/35] #344 improvements to text fields --- graphics/elements/form/text_field.lua | 48 +++++++++++++++------------ 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 47d512b..908d3fc 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -1,6 +1,7 @@ -- Text Value Entry Graphics Element local util = require("scada-common.util") +local events = require("graphics.events") local core = require("graphics.core") local element = require("graphics.element") @@ -33,14 +34,13 @@ local function text_field(args) -- set initial value e.value = args.value or "" - local max_len = 1000 -- temporary - + local max_len = args.max_len or e.frame.w local frame_start = 1 local visible_text = e.value local cursor_pos = string.len(visible_text) + 1 local function frame__update_visible() - visible_text = string.sub(e.value, frame_start, frame_start + math.min(string.len(e.value), args.width) - 1) + visible_text = string.sub(e.value, frame_start, frame_start + math.min(string.len(e.value), e.frame.w) - 1) end -- draw input @@ -89,14 +89,14 @@ local function text_field(args) local function frame__try_lshift() if frame_start > 1 then frame_start = frame_start - 1 - show() + return true end end local function frame__try_rshift() - if (frame_start + args.width - 1) < string.len(e.value) then + if (frame_start + e.frame.w - 1) < string.len(e.value) then frame_start = frame_start + 1 - show() + return true end end @@ -104,12 +104,14 @@ local function text_field(args) ---@param event mouse_interaction mouse event function e.handle_mouse(event) -- only handle if on an increment or decrement arrow - if e.enabled and core.events.was_clicked(event.type) then - e.req_focus() + if e.enabled then + if core.events.was_clicked(event.type) then + e.req_focus() - if event.type == MOUSE_CLICK.UP then - cursor_pos = math.min(event.current.x, string.len(visible_text) + 1) - show() + if event.type == MOUSE_CLICK.UP then + cursor_pos = math.min(event.current.x, string.len(visible_text) + 1) + show() + end end end end @@ -123,25 +125,27 @@ local function text_field(args) if cursor_pos <= string.len(visible_text) then cursor_pos = cursor_pos + 1 show() - else frame__try_rshift() end + elseif frame__try_rshift() then show() end elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then if (event.key == keys.backspace or event.key == keys.delete) then - -- remove charcter at cursor - e.value = string.sub(e.value, 1, frame_start + cursor_pos - 3) .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - show() - else frame__try_lshift() end + -- remove charcter at cursor if there is anything to remove + if frame_start + cursor_pos > 2 then + e.value = string.sub(e.value, 1, frame_start + cursor_pos - 3) .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) + if cursor_pos > 1 then + cursor_pos = cursor_pos - 1 + show() + elseif frame__try_lshift() then show() end + end elseif event.key == keys.left then if cursor_pos > 1 then cursor_pos = cursor_pos - 1 show() - else frame__try_lshift() end + elseif frame__try_lshift() then show() end elseif event.key == keys.right then if cursor_pos <= string.len(visible_text) then cursor_pos = cursor_pos + 1 show() - else frame__try_rshift() end + elseif frame__try_rshift() then show() end end end end @@ -149,8 +153,8 @@ local function text_field(args) -- set the value ---@param val string string to show function e.set_value(val) - e.value = val - frame_start = 1 + math.max(0, string.len(val) - args.width) + e.value = string.sub(val, 1, math.min(max_len, string.len(val))) + frame_start = 1 + math.max(0, string.len(val) - e.frame.w) frame__update_visible() cursor_pos = string.len(visible_text) + 1 show() From 09ab60f79d99568a522abdc992b31dabab249c13 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 00:11:45 -0400 Subject: [PATCH 09/35] #344 double click support --- graphics/events.lua | 56 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/graphics/events.lua b/graphics/events.lua index df5f7eb..603dfee 100644 --- a/graphics/events.lua +++ b/graphics/events.lua @@ -4,16 +4,20 @@ local util = require("scada-common.util") +local DOUBLE_CLICK_MS = 500 + local events = {} ---@enum CLICK_BUTTON -events.CLICK_BUTTON = { +local CLICK_BUTTON = { GENERIC = 0, LEFT_BUTTON = 1, RIGHT_BUTTON = 2, MID_BUTTON = 3 } +events.CLICK_BUTTON = CLICK_BUTTON + ---@enum MOUSE_CLICK local MOUSE_CLICK = { TAP = 1, -- screen tap (complete click) @@ -21,7 +25,8 @@ local MOUSE_CLICK = { UP = 3, -- button up (completed a click) DRAG = 4, -- mouse dragged SCROLL_DOWN = 5, -- scroll down - SCROLL_UP = 6 -- scroll up + SCROLL_UP = 6, -- scroll up + DOUBLE_CLICK = 7 -- double left click } events.MOUSE_CLICK = MOUSE_CLICK @@ -65,7 +70,11 @@ local handler = { -- keyboard modifiers shift = false, alt = false, - ctrl = false + ctrl = false, + -- double click tracking + dc_start = 0, + dc_step = 1, + dc_coord = _coord2d(0, 0) } -- create a new monitor touch mouse interaction event @@ -77,7 +86,7 @@ local handler = { local function _monitor_touch(monitor, x, y) return { monitor = monitor, - button = events.CLICK_BUTTON.GENERIC, + button = CLICK_BUTTON.GENERIC, type = MOUSE_CLICK.TAP, initial = _coord2d(x, y), current = _coord2d(x, y) @@ -112,7 +121,7 @@ end function events.mouse_generic(type, x, y) return { monitor = "", - button = events.CLICK_BUTTON.GENERIC, + button = CLICK_BUTTON.GENERIC, type = type, initial = _coord2d(x, y), current = _coord2d(x, y) @@ -148,25 +157,54 @@ function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.U ---@param y integer y coordinate ---@return mouse_interaction|nil function events.new_mouse_event(event_type, opt, x, y) + local h = handler + if event_type == "mouse_click" then ---@cast opt 1|2|3 - handler.button_down[opt] = _coord2d(x, y) + + local init = true + + if opt == 1 and (h.dc_step % 2) == 1 then + if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then + init = false + h.dc_step = h.dc_step + 1 + end + end + + if init then + h.dc_start = util.time_ms() + h.dc_coord = _coord2d(x, y) + h.dc_step = 2 + end + + h.button_down[opt] = _coord2d(x, y) return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y) elseif event_type == "mouse_up" then ---@cast opt 1|2|3 - local initial = handler.button_down[opt] ---@type coordinate_2d + + if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and + (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then + if h.dc_step == 4 then + util.push_event("double_click", 1, x, y) + h.dc_step = 1 + else h.dc_step = h.dc_step + 1 end + else h.dc_step = 1 end + + local initial = h.button_down[opt] ---@type coordinate_2d return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y) elseif event_type == "monitor_touch" then ---@cast opt string return _monitor_touch(opt, x, y) elseif event_type == "mouse_drag" then ---@cast opt 1|2|3 - local initial = handler.button_down[opt] ---@type coordinate_2d + local initial = h.button_down[opt] ---@type coordinate_2d return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y) elseif event_type == "mouse_scroll" then ---@cast opt 1|-1 local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP) - return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y) + return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y) + elseif event_type == "double_click" then + return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y) end end From f9d0ef60b43a2ff0769fb954b793fc6c9b065daf Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 12:49:31 -0400 Subject: [PATCH 10/35] #344 select all and improved input fields --- graphics/core.lua | 178 ++++++++++++++++++++++++ graphics/elements/form/number_field.lua | 73 ++++++---- graphics/elements/form/text_field.lua | 127 +++-------------- 3 files changed, 246 insertions(+), 132 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index 2305990..3878288 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -111,4 +111,182 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr) } end +-- Interactive Field Manager + +---@param e graphics_base +---@param max_len any +---@param fg_bg any +---@param dis_fg_bg any +function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) + local self = { + frame_start = 1, + visible_text = e.value, + cursor_pos = string.len(e.value) + 1, + selected_all = false + } + + -- update visible text + local function _update_visible() + self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1) + end + + -- try shifting frame left + local function _try_lshift() + if self.frame_start > 1 then + self.frame_start = self.frame_start - 1 + return true + end + end + + -- try shifting frame right + local function _try_rshift() + if (self.frame_start + e.frame.w - 1) < string.len(e.value) then + self.frame_start = self.frame_start + 1 + return true + end + end + + ---@class ifield + local public = {} + + -- show the field + function public.show() + _update_visible() + + if e.enabled then + e.w_set_bkg(fg_bg.bkg) + e.w_set_fgd(fg_bg.fgd) + else + e.w_set_bkg(dis_fg_bg.bkg) + e.w_set_fgd(dis_fg_bg.fgd) + end + + -- clear and print + e.w_set_cur(1, 1) + e.w_write(string.rep(" ", e.frame.w)) + e.w_set_cur(1, 1) + + if e.is_focused() and e.enabled then + -- write text with cursor + if self.selected_all then + e.w_set_bkg(fg_bg.fgd) + e.w_set_fgd(fg_bg.bkg) + e.w_write(self.visible_text) + elseif self.cursor_pos == (string.len(self.visible_text) + 1) then + -- write text with cursor at the end, no need to blit + e.w_write(self.visible_text) + e.w_set_fgd(colors.lightGray) + e.w_write("_") + else + local a, b = "", "" + + if self.cursor_pos <= string.len(self.visible_text) then + a = fg_bg.blit_bkg + b = fg_bg.blit_fgd + end + + local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos) + local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos) + + e.w_blit(self.visible_text, b_fgd, b_bkg) + end + else + self.selected_all = false + + -- write text without cursor + e.w_write(self.visible_text) + end + end + + -- move cursor to x + ---@param x integer + function public.move_cursor(x) + self.selected_all = false + self.cursor_pos = math.min(x, string.len(self.visible_text) + 1) + public.show() + end + + -- select all text + function public.select_all() + self.selected_all = true + public.show() + end + + -- set field value + ---@param val string + function public.set_value(val) + e.value = string.sub(val, 1, math.min(max_len, string.len(val))) + + self.selected_all = false + self.frame_start = 1 + math.max(0, string.len(val) - e.frame.w) + + _update_visible() + self.cursor_pos = string.len(self.visible_text) + 1 + + public.show() + end + + -- try to insert a character if there is space + ---@param char string + function public.try_insert_char(char) + -- limit length + if string.len(e.value) >= max_len then return end + + -- replace if selected all, insert otherwise + if self.selected_all then + self.selected_all = false + self.cursor_pos = 2 + self.frame_start = 1 + + e.value = char + public.show() + else + e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value)) + _update_visible() + + if self.cursor_pos <= string.len(self.visible_text) then + self.cursor_pos = self.cursor_pos + 1 + public.show() + elseif _try_rshift() then public.show() end + end + end + + -- remove charcter before cursor if there is anything to remove, or delete all if selected all + function public.backspace() + if self.selected_all then + self.selected_all = false + e.value = "" + self.cursor_pos = 1 + self.frame_start = 1 + public.show() + else + if self.frame_start + self.cursor_pos > 2 then + e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value)) + if self.cursor_pos > 1 then + self.cursor_pos = self.cursor_pos - 1 + public.show() + elseif _try_lshift() then public.show() end + end + end + end + + -- move cursor left by one + function public.nav_left() + if self.cursor_pos > 1 then + self.cursor_pos = self.cursor_pos - 1 + public.show() + elseif _try_lshift() then public.show() end + end + + -- move cursor right by one + function public.nav_right() + if self.cursor_pos <= string.len(self.visible_text) then + self.cursor_pos = self.cursor_pos + 1 + public.show() + elseif _try_rshift() then public.show() end + end + + return public +end + return core diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index e0e2426..ee75224 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -1,11 +1,10 @@ -- Numeric Value Entry Graphics Element -local util = require("scada-common.util") - local core = require("graphics.core") local element = require("graphics.element") local KEY_CLICK = core.events.KEY_CLICK +local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class number_field_args ---@field default? number default value, defaults to 0 @@ -38,7 +37,11 @@ local function number_field(args) args.max_digits = args.max_digits or e.frame.w -- set initial value - e.value = util.strval(args.default or 0) + e.value = "" .. (args.default or 0) + + -- make an interactive field manager + local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg) + -- draw input local function show() @@ -67,8 +70,16 @@ local function number_field(args) ---@param event mouse_interaction mouse event function e.handle_mouse(event) -- only handle if on an increment or decrement arrow - if e.enabled and core.events.was_clicked(event.type) then - e.req_focus() + if e.enabled then + if core.events.was_clicked(event.type) then + e.req_focus() + + if event.type == MOUSE_CLICK.UP then + ifield.move_cursor(event.current.x) + end + elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then + ifield.select_all() + end end end @@ -77,44 +88,52 @@ local function number_field(args) function e.handle_key(event) if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then if tonumber(event.name) then - e.value = util.trinary(e.value == "0", "", e.value) .. tonumber(event.name) - show() + if e.value == 0 then e.value = "" end + ifield.try_insert_char(event.name) end elseif event.type == KEY_CLICK.DOWN then if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then - e.value = string.sub(e.value, 1, string.len(e.value) - 1) + ifield.backspace() has_decimal = string.find(e.value, "%.") ~= nil - show() elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then - e.value = e.value .. "." has_decimal = true - show() + ifield.try_insert_char(".") elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then - e.value = "-" - show() + ifield.set_value("-") + elseif event.key == keys.left then + ifield.nav_left() + elseif event.key == keys.right then + ifield.nav_right() + elseif event.key == keys.a and event.ctrl then + ifield.select_all() end end end - -- set the value + -- set the value (must be a number) ---@param val number number to show function e.set_value(val) - e.value = val - show() + if tonumber(val) then + ifield.set_value("" .. tonumber(val)) + end end -- set minimum input value ---@param min integer minimum allowed value - function e.set_min(min) - args.min = min - show() - end + function e.set_min(min) args.min = min end -- set maximum input value ---@param max integer maximum allowed value - function e.set_max(max) - args.max = max - show() + function e.set_max(max) args.max = max end + + -- replace text with pasted text if its a number + ---@param text string string pasted + function e.handle_paste(text) + if tonumber(text) then + ifield.set_value("" .. tonumber(text)) + else + ifield.set_value("0") + end end -- handle focused @@ -136,15 +155,15 @@ local function number_field(args) e.value = "" end - show() + ifield.show() end -- on enable/disable - e.enable = show - e.disable = show + e.enable = ifield.show + e.disable = ifield.show -- initial draw - show() + ifield.show() return e.complete() end diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 908d3fc..37a91d3 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -1,8 +1,5 @@ -- Text Value Entry Graphics Element -local util = require("scada-common.util") -local events = require("graphics.events") - local core = require("graphics.core") local element = require("graphics.element") @@ -34,71 +31,8 @@ local function text_field(args) -- set initial value e.value = args.value or "" - local max_len = args.max_len or e.frame.w - local frame_start = 1 - local visible_text = e.value - local cursor_pos = string.len(visible_text) + 1 - - local function frame__update_visible() - visible_text = string.sub(e.value, frame_start, frame_start + math.min(string.len(e.value), e.frame.w) - 1) - end - - -- draw input - local function show() - frame__update_visible() - - if e.enabled then - e.w_set_bkg(args.fg_bg.bkg) - e.w_set_fgd(args.fg_bg.fgd) - else - e.w_set_bkg(args.dis_fg_bg.bkg) - e.w_set_fgd(args.dis_fg_bg.fgd) - end - - -- clear and print - e.w_set_cur(1, 1) - e.w_write(string.rep(" ", e.frame.w)) - e.w_set_cur(1, 1) - - if e.is_focused() and e.enabled then - -- write text with cursor - if cursor_pos == (string.len(visible_text) + 1) then - -- write text with cursor at the end, no need to blit - e.w_write(visible_text) - e.w_set_fgd(colors.lightGray) - e.w_write("_") - else - local a, b = "", "" - - if cursor_pos <= string.len(visible_text) then - a = args.fg_bg.blit_bkg - b = args.fg_bg.blit_fgd - end - - local b_fgd = string.rep(args.fg_bg.blit_fgd, cursor_pos - 1) .. a .. string.rep(args.fg_bg.blit_fgd, string.len(visible_text) - cursor_pos) - local b_bkg = string.rep(args.fg_bg.blit_bkg, cursor_pos - 1) .. b .. string.rep(args.fg_bg.blit_bkg, string.len(visible_text) - cursor_pos) - - e.w_blit(visible_text, b_fgd, b_bkg) - end - else - -- write text without cursor - e.w_write(visible_text) - end - end - - local function frame__try_lshift() - if frame_start > 1 then - frame_start = frame_start - 1 - return true - end - end - - local function frame__try_rshift() - if (frame_start + e.frame.w - 1) < string.len(e.value) then - frame_start = frame_start + 1 - return true - end - end + -- make an interactive field manager + local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg) -- handle mouse interaction ---@param event mouse_interaction mouse event @@ -109,9 +43,10 @@ local function text_field(args) e.req_focus() if event.type == MOUSE_CLICK.UP then - cursor_pos = math.min(event.current.x, string.len(visible_text) + 1) - show() + ifield.move_cursor(event.current.x) end + elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then + ifield.select_all() end end end @@ -119,61 +54,43 @@ local function text_field(args) -- handle keyboard interaction ---@param event key_interaction key event function e.handle_key(event) - if event.type == KEY_CLICK.CHAR and string.len(e.value) < max_len then - e.value = string.sub(e.value, 1, frame_start + cursor_pos - 2) .. event.name .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) - frame__update_visible() - if cursor_pos <= string.len(visible_text) then - cursor_pos = cursor_pos + 1 - show() - elseif frame__try_rshift() then show() end + if event.type == KEY_CLICK.CHAR then + ifield.try_insert_char(event.name) elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then if (event.key == keys.backspace or event.key == keys.delete) then - -- remove charcter at cursor if there is anything to remove - if frame_start + cursor_pos > 2 then - e.value = string.sub(e.value, 1, frame_start + cursor_pos - 3) .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - show() - elseif frame__try_lshift() then show() end - end + ifield.backspace() elseif event.key == keys.left then - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - show() - elseif frame__try_lshift() then show() end + ifield.nav_left() elseif event.key == keys.right then - if cursor_pos <= string.len(visible_text) then - cursor_pos = cursor_pos + 1 - show() - elseif frame__try_rshift() then show() end + ifield.nav_right() + elseif event.key == keys.a and event.ctrl then + ifield.select_all() end end end -- set the value - ---@param val string string to show + ---@param val string string to set function e.set_value(val) - e.value = string.sub(val, 1, math.min(max_len, string.len(val))) - frame_start = 1 + math.max(0, string.len(val) - e.frame.w) - frame__update_visible() - cursor_pos = string.len(visible_text) + 1 - show() + ifield.set_value(val) end + -- replace text with pasted text + ---@param text string string to set function e.handle_paste(text) - e.set_value(text) + ifield.set_value(text) end -- handle focus - e.on_focused = show - e.on_unfocused = show + e.on_focused = ifield.show + e.on_unfocused = ifield.show -- on enable/disable - e.enable = show - e.disable = show + e.enable = ifield.show + e.disable = ifield.show -- initial draw - show() + ifield.show() return e.complete() end From 70b03896d537dc121fa89347740785f54da2c302 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 12:53:05 -0400 Subject: [PATCH 11/35] added double click to custom events --- scada-common/types.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scada-common/types.lua b/scada-common/types.lua index a78b5a2..da872a2 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -270,6 +270,7 @@ types.ALARM_STATE_NAMES = { ---| "mouse_drag" ---| "mouse_scroll" ---| "mouse_up" +---| "double_click" (custom) ---| "paste" ---| "peripheral" ---| "peripheral_detach" @@ -285,7 +286,7 @@ types.ALARM_STATE_NAMES = { ---| "websocket_failure" ---| "websocket_message" ---| "websocket_success" ----| "clock_start" custom, added for reactor PLC +---| "clock_start" (custom) ---@alias fluid ---| "mekanism:empty_gas" From 9cef6e61755f151a35fba0bc91cc5016366b47ed Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 12:58:09 -0400 Subject: [PATCH 12/35] #344 added double click events to event handlers --- coordinator/startup.lua | 4 ++-- pocket/startup.lua | 5 +++-- reactor-plc/configure.lua | 2 +- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 3 ++- rtu/startup.lua | 2 +- rtu/threads.lua | 3 ++- supervisor/startup.lua | 5 +++-- 8 files changed, 15 insertions(+), 11 deletions(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 4d06b3d..145f77b 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.0.13" +local COORDINATOR_VERSION = "v1.0.14" local println = util.println local println_ts = util.println_ts @@ -358,7 +358,7 @@ local function main() sounder.stop() end elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or - event == "mouse_drag" or event == "mouse_scroll" then + event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) elseif event == "speaker_audio_empty" then diff --git a/pocket/startup.lua b/pocket/startup.lua index cfd792b..0592d6c 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "v0.6.1-alpha" +local POCKET_VERSION = "v0.6.2-alpha" local println = util.println local println_ts = util.println_ts @@ -171,7 +171,8 @@ local function main() -- got a packet local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) pocket_comms.handle_packet(packet) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + event == "double_click" then -- handle a monitor touch event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) end diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 3b51c5e..7505b3f 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -210,7 +210,7 @@ function configurator.configure() if event == "timer" then -- notify timer callback dispatcher if no other timer case claimed this event tcd.handle(param1) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event local m_e = core.events.new_mouse_event(event, param1, param2, param3) if m_e then display.handle_mouse(m_e) end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index ac20379..e008280 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -19,7 +19,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.5.8" +local R_PLC_VERSION = "v1.5.9" local println = util.println local println_ts = util.println_ts diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 3b06904..ccbc196 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -265,7 +265,8 @@ function threads.thread__main(smem, init) -- update indicators databus.tx_hw_status(plc_state) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) elseif event == "clock_start" then diff --git a/rtu/startup.lua b/rtu/startup.lua index 00c6406..1ed19e3 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.6.4" +local RTU_VERSION = "v1.6.5" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE diff --git a/rtu/threads.lua b/rtu/threads.lua index bed6294..29e54cf 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -279,7 +279,8 @@ function threads.thread__main(smem) end end end - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) elseif event == "speaker_audio_empty" then diff --git a/supervisor/startup.lua b/supervisor/startup.lua index a09e058..4cc4195 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.0.4" +local SUPERVISOR_VERSION = "v1.0.5" local println = util.println local println_ts = util.println_ts @@ -214,7 +214,8 @@ local function main() -- got a packet local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) superv_comms.handle_packet(packet) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) end From 1f9743efd0a0a958c569ca96b1642f7887874b28 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 13:45:00 -0400 Subject: [PATCH 13/35] #344 don't hide cursor at end of input length --- graphics/core.lua | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index 3878288..e9cacd4 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -140,7 +140,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) -- try shifting frame right local function _try_rshift() - if (self.frame_start + e.frame.w - 1) < string.len(e.value) then + if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then self.frame_start = self.frame_start + 1 return true end @@ -172,7 +172,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) e.w_set_bkg(fg_bg.fgd) e.w_set_fgd(fg_bg.bkg) e.w_write(self.visible_text) - elseif self.cursor_pos == (string.len(self.visible_text) + 1) then + elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then -- write text with cursor at the end, no need to blit e.w_write(self.visible_text) e.w_set_fgd(colors.lightGray) @@ -218,7 +218,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) e.value = string.sub(val, 1, math.min(max_len, string.len(val))) self.selected_all = false - self.frame_start = 1 + math.max(0, string.len(val) - e.frame.w) + self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2) _update_visible() self.cursor_pos = string.len(self.visible_text) + 1 @@ -243,11 +243,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) else e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value)) _update_visible() - - if self.cursor_pos <= string.len(self.visible_text) then - self.cursor_pos = self.cursor_pos + 1 - public.show() - elseif _try_rshift() then public.show() end + public.nav_right() end end @@ -280,7 +276,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) -- move cursor right by one function public.nav_right() - if self.cursor_pos <= string.len(self.visible_text) then + if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then self.cursor_pos = self.cursor_pos + 1 public.show() elseif _try_rshift() then public.show() end From 645a5f5137f90ec1ab1bca756a262f55a5973e2e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 14:31:37 -0400 Subject: [PATCH 14/35] #344 added focus navigation to checkboxes and radio buttons, refactor of enable handlers --- graphics/element.lua | 16 +++---- graphics/elements/controls/checkbox.lua | 41 ++++++++++++++-- graphics/elements/controls/hazard_button.lua | 4 +- graphics/elements/controls/push_button.lua | 6 +-- graphics/elements/controls/radio_button.lua | 48 +++++++++++++++++-- .../elements/controls/spinbox_numeric.lua | 4 +- graphics/elements/form/number_field.lua | 6 +-- graphics/elements/form/text_field.lua | 6 +-- 8 files changed, 100 insertions(+), 31 deletions(-) diff --git a/graphics/element.lua b/graphics/element.lua index f356c03..80182a9 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -321,6 +321,12 @@ function element.new(args, child_offset_x, child_offset_y) ---@param id element_id element identifier function protected.on_removed(id) end + -- handle enabled + function protected.on_enabled() end + + -- handle disabled + function protected.on_disabled() end + -- handle this element having been focused function protected.on_focused() end @@ -369,12 +375,6 @@ function element.new(args, child_offset_x, child_offset_y) ---@param max integer maximum allowed value function protected.set_max(max) end - -- enable the control - function protected.enable() end - - -- disable the control - function protected.disable() end - -- custom recolor command, varies by element if implemented ---@vararg cpair|color color(s) function protected.recolor(...) end @@ -599,7 +599,7 @@ function element.new(args, child_offset_x, child_offset_y) function public.enable() if not protected.enabled then protected.enabled = true - protected.enable() + protected.on_enabled() end end @@ -607,7 +607,7 @@ function element.new(args, child_offset_x, child_offset_y) function public.disable() if protected.enabled then protected.enabled = false - protected.disable() + protected.on_disabled() public.unfocus_all() end end diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/checkbox.lua index 77f9808..1d9e1c7 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -22,6 +22,7 @@ local function checkbox(args) assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field") assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field") + args.can_focus = true args.height = 1 args.width = 3 + string.len(args.label) @@ -53,6 +54,21 @@ local function checkbox(args) end end + -- write label text + local function draw_label() + if e.enabled and e.is_focused() then + e.w_set_cur(3, 1) + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.fgd) + e.w_write(args.label) + else + e.w_set_cur(3, 1) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + e.w_write(args.label) + end + end + -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) @@ -63,6 +79,18 @@ local function checkbox(args) end end + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == core.events.KEY_CLICK.DOWN then + if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then + e.value = not e.value + draw() + args.callback(e.value) + end + end + end + -- set the value ---@param val integer new value function e.set_value(val) @@ -70,14 +98,17 @@ local function checkbox(args) draw() end - -- write label text - e.w_set_cur(3, 1) - e.w_set_fgd(e.fg_bg.fgd) - e.w_set_bkg(e.fg_bg.bkg) - e.w_write(args.label) + -- handle focus + e.on_focused = draw_label + e.on_unfocused = draw_label + + -- handle enable + e.on_enabled = draw_label + e.on_disabled = draw_label -- initial draw draw() + draw_label() return e.complete() end diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index f6ac2d3..92e4ac5 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -178,7 +178,7 @@ local function hazard_button(args) end -- show the button as disabled - function e.disable() + function e.on_disabled() if args.dis_colors then draw_border(args.dis_colors.color_a) e.w_set_fgd(args.dis_colors.color_b) @@ -188,7 +188,7 @@ local function hazard_button(args) end -- show the button as enabled - function e.enable() + function e.on_enabled() draw_border(args.accent) e.w_set_fgd(args.fg_bg.fgd) e.w_set_cur(3, 2) diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index b8e3013..cab0977 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -112,7 +112,7 @@ local function push_button(args) end -- show butten as enabled - function e.enable() + function e.on_enabled() if args.dis_fg_bg ~= nil then e.value = false e.w_set_fgd(e.fg_bg.fgd) @@ -122,7 +122,7 @@ local function push_button(args) end -- show button as disabled - function e.disable() + function e.on_disabled() if args.dis_fg_bg ~= nil then e.value = false e.w_set_fgd(args.dis_fg_bg.fgd) @@ -131,7 +131,7 @@ local function push_button(args) end end - -- handle focus change + -- handle focus e.on_focused = show_pressed e.on_unfocused = show_unpressed diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 33a65d5..26c699b 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -33,9 +33,6 @@ local function radio_button(args) assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "graphics.elements.controls.radio_button: min_width must be nil or a number > 0") - -- one line per option - args.height = #args.options - -- determine widths local max_width = 1 for i = 1, #args.options do @@ -47,11 +44,16 @@ local function radio_button(args) local button_text_width = math.max(max_width, args.min_width or 0) + -- set automatic args + args.can_focus = true args.width = button_text_width + 2 + args.height = #args.options -- one line per option -- create new graphics element base object local e = element.new(args) + local focused_opt = 1 + -- button state (convert nil to 1 if missing) e.value = args.default or 1 @@ -74,8 +76,14 @@ local function radio_button(args) e.w_write("\x95") -- write button text - e.w_set_fgd(e.fg_bg.fgd) - e.w_set_bkg(e.fg_bg.bkg) + if i == focused_opt and e.is_focused() and e.enabled then + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.fgd) + else + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + end + e.w_write(opt) end end @@ -93,6 +101,28 @@ local function radio_button(args) end end + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == core.events.KEY_CLICK.DOWN then + if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then + e.value = focused_opt + draw() + args.callback(e.value) + elseif event.key == keys.down then + if focused_opt < #args.options then + focused_opt = focused_opt + 1 + draw() + end + elseif event.key == keys.up then + if focused_opt > 1 then + focused_opt = focused_opt - 1 + draw() + end + end + end + end + -- set the value ---@param val integer new value function e.set_value(val) @@ -100,6 +130,14 @@ local function radio_button(args) draw() end + -- handle focus + e.on_focused = draw + e.on_unfocused = draw + + -- handle enable + e.on_enabled = draw + e.on_disabled = draw + -- initial draw draw() diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua index 76f6c06..f7d0ee5 100644 --- a/graphics/elements/controls/spinbox_numeric.lua +++ b/graphics/elements/controls/spinbox_numeric.lua @@ -176,12 +176,12 @@ local function spinbox(args) end -- enable this input - function e.enable() + function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end -- disable this input - function e.disable() + function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index ee75224..43554ce 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -158,9 +158,9 @@ local function number_field(args) ifield.show() end - -- on enable/disable - e.enable = ifield.show - e.disable = ifield.show + -- handle enable + e.on_enabled = ifield.show + e.on_disabled = ifield.show -- initial draw ifield.show() diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 37a91d3..ebeb35a 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -85,9 +85,9 @@ local function text_field(args) e.on_focused = ifield.show e.on_unfocused = ifield.show - -- on enable/disable - e.enable = ifield.show - e.disable = ifield.show + -- handle enable + e.on_enabled = ifield.show + e.on_disabled = ifield.show -- initial draw ifield.show() From 18bcfb4014d4eafc0cefccaeb19b2ec8745eda66 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 15:30:53 -0400 Subject: [PATCH 15/35] #344 nav to start/end of fields --- graphics/core.lua | 24 ++++++++++++++++-------- graphics/elements/form/number_field.lua | 4 ++++ graphics/elements/form/text_field.lua | 4 ++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index e9cacd4..46110c2 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -216,14 +216,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) ---@param val string function public.set_value(val) e.value = string.sub(val, 1, math.min(max_len, string.len(val))) - - self.selected_all = false - self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2) - - _update_visible() - self.cursor_pos = string.len(self.visible_text) + 1 - - public.show() + public.nav_end() end -- try to insert a character if there is space @@ -282,6 +275,21 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) elseif _try_rshift() then public.show() end end + -- move cursor to the start + function public.nav_start() + self.cursor_pos = 1 + self.frame_start = 1 + public.show() + end + + -- move cursor to the end + function public.nav_end() + self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2) + _update_visible() + self.cursor_pos = string.len(self.visible_text) + 1 + public.show() + end + return public end diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index 43554ce..3c920b8 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -106,6 +106,10 @@ local function number_field(args) ifield.nav_right() elseif event.key == keys.a and event.ctrl then ifield.select_all() + elseif event.key == keys.home or event.key == keys.up then + ifield.nav_start() + elseif event.key == keys["end"] or event.key == keys.down then + ifield.nav_end() end end end diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index ebeb35a..7ac786a 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -65,6 +65,10 @@ local function text_field(args) ifield.nav_right() elseif event.key == keys.a and event.ctrl then ifield.select_all() + elseif event.key == keys.home or event.key == keys.up then + ifield.nav_start() + elseif event.key == keys["end"] or event.key == keys.down then + ifield.nav_end() end end end From 689d4747962af7302f2e30979aadeb09bd62fbb1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 16:45:33 -0400 Subject: [PATCH 16/35] #344 support hiding characters in text fields --- graphics/core.lua | 33 +++++++++++++++++++++++---- graphics/elements/form/text_field.lua | 8 +++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index 46110c2..ee7d829 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -149,6 +149,19 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) ---@class ifield local public = {} + -- censor the display (for private info, for example) with the provided character
+ -- disable by passing no argument + ---@param censor string? character to hide data with + function public.censor(censor) + if type(censor) == "string" and string.len(censor) == 1 then + self.censor = censor + public.show() + else + self.censor = nil + public.show() + end + end + -- show the field function public.show() _update_visible() @@ -166,15 +179,23 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) e.w_write(string.rep(" ", e.frame.w)) e.w_set_cur(1, 1) + local function _write() + if self.censor then + e.w_write(string.rep(self.censor, string.len(self.visible_text))) + else + e.w_write(self.visible_text) + end + end + if e.is_focused() and e.enabled then -- write text with cursor if self.selected_all then e.w_set_bkg(fg_bg.fgd) e.w_set_fgd(fg_bg.bkg) - e.w_write(self.visible_text) + _write() elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then -- write text with cursor at the end, no need to blit - e.w_write(self.visible_text) + _write() e.w_set_fgd(colors.lightGray) e.w_write("_") else @@ -188,13 +209,17 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos) local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos) - e.w_blit(self.visible_text, b_fgd, b_bkg) + if self.censor then + e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg) + else + e.w_blit(self.visible_text, b_fgd, b_bkg) + end end else self.selected_all = false -- write text without cursor - e.w_write(self.visible_text) + _write() end end diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 7ac786a..3211676 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -9,6 +9,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class text_field_args ---@field value? string initial value ---@field max_len? integer maximum string length +---@field censor? string character to replace text with when printing to screen ---@field dis_fg_bg? cpair foreground/background colors when disabled ---@field parent graphics_element ---@field id? string element id @@ -20,7 +21,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK -- new text entry field ---@param args text_field_args ----@return graphics_element element, element_id id +---@return graphics_element element, element_id id, function censor_ctl local function text_field(args) args.height = 1 args.can_focus = true @@ -34,6 +35,8 @@ local function text_field(args) -- make an interactive field manager local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg) + ifield.censor(args.censor) + -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) @@ -96,7 +99,8 @@ local function text_field(args) -- initial draw ifield.show() - return e.complete() + local elem, id = e.complete() + return elem, id, ifield.censor end return text_field From 8ab1307b2b4cad8c0696e002fe101e84c455d896 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 16:50:54 -0400 Subject: [PATCH 17/35] #344 include holding down keys for number fields --- graphics/elements/form/number_field.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index 3c920b8..3e2d350 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -91,7 +91,7 @@ local function number_field(args) if e.value == 0 then e.value = "" end ifield.try_insert_char(event.name) end - elseif event.type == KEY_CLICK.DOWN then + elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then ifield.backspace() has_decimal = string.find(e.value, "%.") ~= nil From 881a120d3436299bb54112efba53c35f7b52bca0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Sep 2023 20:22:02 -0400 Subject: [PATCH 18/35] #145 more work on plc configurator --- graphics/element.lua | 13 +++ reactor-plc/configure.lua | 226 +++++++++++++++++++++++++++++++++----- 2 files changed, 209 insertions(+), 30 deletions(-) diff --git a/graphics/element.lua b/graphics/element.lua index 80182a9..ea32d0d 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -522,6 +522,19 @@ function element.new(args, child_offset_x, child_offset_y) end end + -- remove all child elements and reset next y + function public.remove_all() + for i = 1, #protected.children do + local child = protected.children[i].get() ---@type graphics_element + child.delete() + protected.on_removed(child.get_id()) + end + + self.next_y = 1 + protected.children = {} + protected.child_id_map = {} + end + -- attempt to get a child element by ID (does not include this element itself) ---@nodiscard ---@param id element_id diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 7505b3f..59a7e4f 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -2,11 +2,14 @@ -- Configuration GUI -- -local core = require("graphics.core") local log = require("scada-common.log") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") log.init("/log.txt", log.MODE.APPEND, true) +local core = require("graphics.core") + local DisplayBox = require("graphics.elements.displaybox") local Div = require("graphics.elements.div") local MultiPane = require("graphics.elements.multipane") @@ -20,15 +23,16 @@ local CheckBox = require("graphics.elements.controls.checkbox") local NumberField = require("graphics.elements.form.number_field") local TextField = require("graphics.elements.form.text_field") -local tcd = require("scada-common.tcd") -local util = require("scada-common.util") +local ListBox = require("graphics.elements.listbox") local print = util.print local println = util.println local cpair = core.cpair +local LEFT = core.TEXT_ALIGN.LEFT local CENTER = core.TEXT_ALIGN.CENTER +local RIGHT = core.TEXT_ALIGN.RIGHT ---@class plc_configurator local configurator = {} @@ -59,11 +63,39 @@ style.colors = { } local tool_ctl = { - networked = false, + need_config = false, set_networked = nil, ---@type function next_from_plc = nil, ---@type function - back_from_log = nil ---@type function + back_from_log = nil, ---@type function + gen_summary = nil ---@type function +} + +local tmp_cfg = { + Networked = false, + UnitID = 0, + SVR_Channel = 0, + PLC_Channel = 0, + ConnTimeout = 0, + TrustedRange = 0, + AuthKey = "", + LogMode = 0, + LogPath = "", + LogDebug = false, +} + +local fields = { + -- printed name, tmp_cfg name, requires_network + { "Networked", "Networked", false }, + { "Unit ID", "UnitID", false }, + { "SVR Channel", "SVR_Channel", true }, + { "PLC Channel", "PLC_Channel", true }, + { "Connection Timeout", "ConnTimeout", true }, + { "Trusted Range", "TrustedRange", true }, + { "Facility Auth Key", "AuthKey", true }, + { "Log Mode", "LogMode", false }, + { "Log Path", "LogPath", false }, + { "Log Debug Messages", "LogDebug", false } } local function _config_view(display) @@ -76,23 +108,34 @@ local function _config_view(display) local plc_cfg = Div{parent=root_pane_div,x=1,y=1} local net_cfg = Div{parent=root_pane_div,x=1,y=1} local log_cfg = Div{parent=root_pane_div,x=1,y=1} + local summary = Div{parent=root_pane_div,x=1,y=1} - local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg}} + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary}} -- MAIN PAGE + local y_offset = 0 + TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."} - PushButton{parent=main_page,x=2,y=5,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=main_page,x=2,y=7,min_width=20,text="View Configuration",callback=function()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=main_page,x=2,y=9,min_width=28,text="Import Legacy 'config.lua'",callback=function()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=cpair(colors.white,colors.gray)} + if tool_ctl.need_config then + y_offset = 3 + TextBox{parent=main_page,x=2,y=5,height=2,text_align=CENTER,text="Notice: This Reactor PLC is not configured. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)} + end + + PushButton{parent=main_page,x=2,y=y_offset+5,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=main_page,x=2,y=y_offset+7,min_width=20,text="View Configuration",callback=function()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} + + if fs.exists("/reactor-plc/config.lua") then + PushButton{parent=main_page,x=2,y=y_offset+9,min_width=28,text="Import Legacy 'config.lua'",callback=function()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=cpair(colors.white,colors.gray)} + end + ---@diagnostic disable-next-line: undefined-field - PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=function()os.queueEvent("exit")end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=function()os.queueEvent("terminate")end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} local nav_fg_bg = cpair(colors.black,colors.white) local nav_a_fg_bg = cpair(colors.white,colors.gray) - -- PLC CONFIG local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49} @@ -116,8 +159,13 @@ local function _config_view(display) TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,text="Unit #"} local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=1,min=1,fg_bg=cpair(colors.black,colors.white)} + local function submit_id() + tmp_cfg.UnitID = u_id.get_value() + tool_ctl.next_from_plc() + end + PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=function()tool_ctl.next_from_plc()end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} -- NET CONFIG @@ -125,7 +173,7 @@ local function _config_view(display) local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49} local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49} - local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2}} + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}} TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} @@ -133,23 +181,53 @@ local function _config_view(display) TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"} - NumberField{parent=net_c_1,x=1,y=9,width=7,default=16240,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} + local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=16240,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,text="PLC Channel"} - NumberField{parent=net_c_1,x=1,y=12,width=7,allow_decimal=true,allow_negative=true,default=16241,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} + local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=16241,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} + local function submit_channels() + tmp_cfg.SVR_Channel = svr_chan.get_value() + tmp_cfg.PLC_Channel = plc_chan.get_value() + net_pane.set_value(2) + end + PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - TextBox{parent=net_c_2,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} - TextBox{parent=net_c_2,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=cpair(colors.gray,colors.lightGray)} + TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,text="Connection Timeout"} + local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=5,min=2,max=25,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=net_c_2,x=9,y=2,height=2,text_align=CENTER,text="seconds (default 5)",fg_bg=cpair(colors.gray,colors.lightGray)} + TextBox{parent=net_c_2,x=1,y=3,height=4,text_align=CENTER,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=cpair(colors.gray,colors.lightGray)} - TextBox{parent=net_c_2,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"} - TextField{parent=net_c_2,x=1,y=12,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=net_c_2,x=1,y=8,height=1,text_align=CENTER,text="Trusted Range"} + local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=0,min=0,max_digits=20,allow_decimal=true,fg_bg=cpair(colors.black,colors.white)} + TextBox{parent=net_c_2,x=1,y=10,height=4,text_align=CENTER,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=cpair(colors.gray,colors.lightGray)} + + local function submit_ct_tr() + tmp_cfg.ConnTimeout = timeout.get_value() + tmp_cfg.TrustedRange = range.get_value() + net_pane.set_value(3) + end PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} + TextBox{parent=net_c_3,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=cpair(colors.gray,colors.lightGray)} + + TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"} + local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} + local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=function(v)censor(util.trinary(v,"*",nil))end} + + local function submit_auth() + tmp_cfg.AuthKey = key.get_value() + main_pane.set_value(4) + end + + PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} -- LOG CONFIG @@ -163,31 +241,112 @@ local function _config_view(display) local mode = RadioButton{parent=log_c_1,x=1,y=4,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"} - TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value="/log.txt",fg_bg=cpair(colors.black,colors.white)} + local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value="/log.txt",max_len=128,fg_bg=cpair(colors.black,colors.white)} - CheckBox{parent=log_c_1,x=1,y=10,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} + local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)} + local function submit_log() + tmp_cfg.LogMode = mode.get_value() + tmp_cfg.LogPath = path.get_value() + tmp_cfg.LogDebug = en_dbg.get_value() + tool_ctl.gen_summary() + main_pane.set_value(5) + end + PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()tool_ctl.back_from_log()end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + + -- SUMMARY OF CHANGES + + local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} + + local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2}} + + TextBox{parent=summary,x=1,y=2,height=1,text_align=CENTER,text=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=cpair(colors.black,colors.white),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} + + local function save_and_continue() + -- settings.load(".plc.settings") + -- settings.set("UnitID", tmp_cfg.UnitID) + -- settings.save(".plc.settings") + sum_pane.set_value(2) + end + + PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=nav_a_fg_bg} + + TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"} + + local function go_home() + main_pane.set_value(1) + plc_pane.set_value(1) + net_pane.set_value(1) + sum_pane.set_value(1) + end + + PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} +---@diagnostic disable-next-line: undefined-field + PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=function()os.queueEvent("terminate")end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} -- overwrite functions now that we have the elements function tool_ctl.set_networked(enable) - tool_ctl.networked = enable + tmp_cfg.Networked = enable if enable then u_id.set_max(4) else u_id.set_max(999) end end function tool_ctl.next_from_plc() - if tool_ctl.networked then main_pane.set_value(3) else main_pane.set_value(4) end + if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end end function tool_ctl.back_from_log() - if tool_ctl.networked then main_pane.set_value(3) else main_pane.set_value(2) end + if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end + end + + function tool_ctl.gen_summary() + setting_list.remove_all() + + local alternate = false + local inner_width = setting_list.get_width() - 1 + + for i = 1, #fields do + local f = fields[i] + local height = 1 + local label_w = string.len(f[1]) + local val_max_w = (inner_width - label_w) + 1 + local val = util.strval(tmp_cfg[f[2]]) + + if f[3] and not tmp_cfg.Networked then val = "n/a" end + if f[2] == "AuthKey" and hide_key.get_value() then val = string.rep("*", string.len(val)) end + + local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white)) + alternate = not alternate + + if (f[2] == "LogPath" or f[2] == "AuthKey") and string.len(val) > val_max_w then + local lines = util.strwrap(val, inner_width) + height = #lines + 1 + end + + local line = Div{parent=setting_list,height=height,fg_bg=c} + TextBox{parent=line,text=f[1],width=string.len(f[1]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + + if height > 1 then + TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT} + else + TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} + end + end end end -function configurator.configure() +function configurator.configure(need_config) + log.debug("configurator started") + + tool_ctl.need_config = true + -- reset terminal term.setTextColor(colors.white) term.setBackgroundColor(colors.black) @@ -203,6 +362,14 @@ function configurator.configure() local display = DisplayBox{window=term.current(),fg_bg=style.root} _config_view(display) + local function clear() + display.delete() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.black) + term.clear() + term.setCursorPos(1, 1) + end + while true do local event, param1, param2, param3 = util.pull_event() @@ -221,12 +388,11 @@ function configurator.configure() elseif event == "paste" then -- handle a paste event display.handle_paste(param1) - elseif event == "exit" then - return end -- check for termination request if event == "terminate" then + clear() println("terminate requested, exiting config") return false end From 70831b49d2afe31f896eaf261ec7cfa8dcd8447f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 24 Sep 2023 22:27:39 -0400 Subject: [PATCH 19/35] #344 2D radio button array --- graphics/element.lua | 1 + graphics/elements/controls/checkbox.lua | 9 +- graphics/elements/controls/radio_2d.lua | 204 ++++++++++++++++++++ graphics/elements/controls/radio_button.lua | 20 +- 4 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 graphics/elements/controls/radio_2d.lua diff --git a/graphics/element.lua b/graphics/element.lua index ea32d0d..e04a0df 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -28,6 +28,7 @@ local element = {} ---|hazard_button_args ---|multi_button_args ---|push_button_args +---|radio_2d_args ---|radio_button_args ---|sidebar_args ---|spinbox_args diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/checkbox.lua index 1d9e1c7..663e18d 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -6,7 +6,7 @@ local element = require("graphics.element") ---@class checkbox_args ---@field label string checkbox text ---@field box_fg_bg cpair colors for checkbox ----@field callback function function to call on press +---@field callback? function function to call on press ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -20,7 +20,6 @@ local element = require("graphics.element") local function checkbox(args) assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field") assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field") - assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field") args.can_focus = true args.height = 1 @@ -72,10 +71,10 @@ local function checkbox(args) -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) - if e.enabled and core.events.was_clicked(event.type) then + if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then e.value = not e.value draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end end end @@ -86,7 +85,7 @@ local function checkbox(args) if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then e.value = not e.value draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end end end end diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/radio_2d.lua new file mode 100644 index 0000000..a3ac712 --- /dev/null +++ b/graphics/elements/controls/radio_2d.lua @@ -0,0 +1,204 @@ +-- 2D Radio Button Graphics Element + +local util = require("scada-common.util") + +local core = require("graphics.core") +local element = require("graphics.element") + +---@class radio_2d_args +---@field rows integer +---@field columns integer +---@field options table +---@field radio_colors cpair radio button colors (inner & outer) +---@field select_color? color color for radio button when selected +---@field color_map? table colors for each radio button when selected +---@field disable_color? color color for radio button when disabled +---@field disable_fg_bg? cpair text colors when disabled +---@field default? integer default state, defaults to options[1] +---@field callback? function function to call on touch +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer auto incremented if omitted +---@field fg_bg? cpair foreground/background colors +---@field hidden? boolean true to hide on initial draw + +-- new 2D radio button list (latch selection, exclusively one color at a time) +---@param args radio_2d_args +---@return graphics_element element, element_id id +local function radio_2d_button(args) + assert(type(args.options) == "table" and #args.options > 0, "graphics.elements.controls.radio_2d: options should be a table with length >= 1") + assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_2d: radio_colors is a required field") + assert(type(args.select_color) == "number" or type(args.color_map) == "table", "graphics.elements.controls.radio_2d: select_color or color_map is required") + assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), + "graphics.elements.controls.radio_2d: default must be nil or a number > 0") + + local array = {} + local col_widths = {} + + local next_idx = 1 + local total_width = 0 + local max_rows = 1 + + local focused_opt = 1 + local focus_x, focus_y = 1, 1 + + -- build table to display + for col = 1, args.columns do + local max_width = 0 + array[col] = {} + + for row = 1, args.rows do + local len = string.len(args.options[next_idx]) + if len > max_width then max_width = len end + if row > max_rows then max_rows = row end + + table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len }) + + next_idx = next_idx + 1 + if next_idx > #args.options then break end + end + + table.insert(col_widths, max_width + 3) + total_width = total_width + max_width + 3 + if next_idx > #args.options then break end + end + + args.can_focus = true + args.width = total_width + args.height = max_rows + + -- create new graphics element base object + local e = element.new(args) + + -- selected option (convert nil to 1 if missing) + e.value = args.default or 1 + + -- show the args.options/states + local function draw() + local col_x = 1 + + local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b) + + for col = 1, #array do + for row = 1, #array[col] do + local opt = array[col][row] + local select_color = args.select_color + + if type(args.color_map) == "table" and args.color_map[opt.id] then + select_color = args.color_map[opt.id] + end + + local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a) + local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b) + + e.w_set_cur(col_x, row) + + e.w_set_fgd(inner_color) + e.w_set_bkg(outer_color) + e.w_write("\x88") + + e.w_set_fgd(outer_color) + e.w_set_bkg(e.fg_bg.bkg) + e.w_write("\x95") + + if opt.id == focused_opt then + focus_x, focus_y = row, col + end + + -- write button text + if opt.id == focused_opt and e.is_focused() and e.enabled then + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.fgd) + elseif type(args.disable_fg_bg) == "table" and not e.enabled then + e.w_set_fgd(args.disable_fg_bg.fgd) + e.w_set_bkg(args.disable_fg_bg.bkg) + else + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + end + + e.w_write(opt.text) + end + + col_x = col_x + col_widths[col] + end + end + + -- handle mouse interaction + ---@param event mouse_interaction mouse event + function e.handle_mouse(event) + if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then + -- determine what was pressed + for _, row in ipairs(array) do + local elem = row[event.current.y] + if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then + e.value = elem.id + focused_opt = elem.id + draw() + if type(args.callback) == "function" then args.callback(e.value) end + break + end + end + end + end + + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then + if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then + e.value = focused_opt + draw() + if type(args.callback) == "function" then args.callback(e.value) end + elseif event.key == keys.down then + if focused_opt < #args.options then + focused_opt = focused_opt + 1 + draw() + end + elseif event.key == keys.up then + if focused_opt > 1 then + focused_opt = focused_opt - 1 + draw() + end + elseif event.key == keys.right then + if array[focus_y + 1] and array[focus_y + 1][focus_x] then + focused_opt = array[focus_y + 1][focus_x].id + else focused_opt = array[1][focus_x].id end + draw() + elseif event.key == keys.left then + if array[focus_y - 1] and array[focus_y - 1][focus_x] then + focused_opt = array[focus_y - 1][focus_x].id + draw() + elseif array[#array][focus_x] then + focused_opt = array[#array][focus_x].id + draw() + end + end + end + end + + -- set the value + ---@param val integer new value + function e.set_value(val) + if val > 0 and val <= #args.options then + e.value = val + draw() + end + end + + -- handle focus + e.on_focused = draw + e.on_unfocused = draw + + -- handle enable + e.on_enabled = draw + e.on_disabled = draw + + -- initial draw + draw() + + return e.complete() +end + +return radio_2d_button diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 26c699b..cff16eb 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -5,13 +5,15 @@ local util = require("scada-common.util") local core = require("graphics.core") local element = require("graphics.element") +local KEY_CLICK = core.events.KEY_CLICK + ---@class radio_button_args ---@field options table button options ----@field callback function function to call on touch ---@field radio_colors cpair radio button colors (inner & outer) ---@field select_color color color for radio button border when selected ---@field default? integer default state, defaults to options[1] ---@field min_width? integer text length + 2 if omitted +---@field callback? function function to call on touch ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -25,7 +27,6 @@ local element = require("graphics.element") local function radio_button(args) assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field") assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required") - assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field") assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_button: radio_colors is a required field") assert(type(args.select_color) == "number", "graphics.elements.controls.radio_button: select_color is a required field") assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), @@ -95,8 +96,9 @@ local function radio_button(args) -- determine what was pressed if args.options[event.current.y] ~= nil then e.value = event.current.y + focused_opt = e.value draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end end end end @@ -104,11 +106,11 @@ local function radio_button(args) -- handle keyboard interaction ---@param event key_interaction key event function e.handle_key(event) - if event.type == core.events.KEY_CLICK.DOWN then - if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then + if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then + if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then e.value = focused_opt draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end elseif event.key == keys.down then if focused_opt < #args.options then focused_opt = focused_opt + 1 @@ -126,8 +128,10 @@ local function radio_button(args) -- set the value ---@param val integer new value function e.set_value(val) - e.value = val - draw() + if val > 0 and val <= #args.options then + e.value = val + draw() + end end -- handle focus From ed4180a072b2e16b6c1297f6b418377a46c200cb Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Sep 2023 19:34:10 -0400 Subject: [PATCH 20/35] #344 element redraws and shorter assert messages --- graphics/element.lua | 5 + graphics/elements/colormap.lua | 13 +- graphics/elements/controls/app.lua | 26 ++- graphics/elements/controls/checkbox.lua | 13 +- graphics/elements/controls/hazard_button.lua | 24 +- graphics/elements/controls/multi_button.lua | 20 +- graphics/elements/controls/push_button.lua | 19 +- graphics/elements/controls/radio_2d.lua | 41 ++-- graphics/elements/controls/radio_button.lua | 34 +-- graphics/elements/controls/sidebar.lua | 26 ++- .../elements/controls/spinbox_numeric.lua | 30 +-- graphics/elements/controls/switch_button.lua | 32 +-- graphics/elements/controls/tabbar.lua | 18 +- graphics/elements/form/number_field.lua | 37 +-- graphics/elements/form/text_field.lua | 7 +- graphics/elements/indicators/alight.lua | 23 +- graphics/elements/indicators/coremap.lua | 15 +- graphics/elements/indicators/data.lua | 31 +-- graphics/elements/indicators/hbar.lua | 21 +- graphics/elements/indicators/icon.lua | 25 ++- graphics/elements/indicators/led.lua | 27 ++- graphics/elements/indicators/ledpair.lua | 32 ++- graphics/elements/indicators/ledrgb.lua | 23 +- graphics/elements/indicators/light.lua | 25 ++- graphics/elements/indicators/power.lua | 35 +-- graphics/elements/indicators/rad.lua | 32 +-- graphics/elements/indicators/state.lua | 22 +- graphics/elements/indicators/trilight.lua | 39 ++-- graphics/elements/indicators/vbar.lua | 33 +-- graphics/elements/listbox.lua | 16 +- graphics/elements/multipane.lua | 17 +- graphics/elements/pipenet.lua | 211 ++++++++++-------- graphics/elements/rectangle.lua | 107 ++++----- graphics/elements/textbox.lua | 20 +- graphics/elements/tiling.lua | 67 +++--- 35 files changed, 610 insertions(+), 556 deletions(-) diff --git a/graphics/element.lua b/graphics/element.lua index e04a0df..20bc0c0 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -360,6 +360,8 @@ function element.new(args, child_offset_x, child_offset_y) ---@param result any function protected.response_callback(result) end + -- accessors and control -- + -- get value ---@nodiscard function protected.get_value() return protected.value end @@ -387,6 +389,9 @@ function element.new(args, child_offset_x, child_offset_y) -- luacheck: pop ---@diagnostic enable: unused-local, unused-vararg + -- re-draw this element + function protected.redraw() end + -- start animations function protected.start_anim() end diff --git a/graphics/elements/colormap.lua b/graphics/elements/colormap.lua index 25fd135..7e3554f 100644 --- a/graphics/elements/colormap.lua +++ b/graphics/elements/colormap.lua @@ -1,7 +1,5 @@ -- Color Map Graphics Element -local util = require("scada-common.util") - local element = require("graphics.element") ---@class colormap_args @@ -16,7 +14,7 @@ local element = require("graphics.element") ---@return graphics_element element, element_id id local function colormap(args) local bkg = "008877FFCCEE114455DD9933BBAA2266" - local spaces = util.spaces(32) + local spaces = string.rep(" ", 32) args.width = 32 args.height = 1 @@ -25,8 +23,13 @@ local function colormap(args) local e = element.new(args) -- draw color map - e.w_set_cur(1, 1) - e.w_blit(spaces, bkg, bkg) + function e.redraw() + e.w_set_cur(1, 1) + e.w_blit(spaces, bkg, bkg) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua index a0d5949..10ef6b5 100644 --- a/graphics/elements/controls/app.lua +++ b/graphics/elements/controls/app.lua @@ -24,21 +24,17 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@param args app_button_args ---@return graphics_element element, element_id id local function app_button(args) - assert(type(args.text) == "string", "graphics.elements.controls.app: text is a required field") - assert(type(args.title) == "string", "graphics.elements.controls.app: title is a required field") - assert(type(args.callback) == "function", "graphics.elements.controls.app: callback is a required field") - assert(type(args.app_fg_bg) == "table", "graphics.elements.controls.app: app_fg_bg is a required field") + assert(type(args.text) == "string", "controls.app: text is a required field") + assert(type(args.title) == "string", "controls.app: title is a required field") + assert(type(args.callback) == "function", "controls.app: callback is a required field") + assert(type(args.app_fg_bg) == "table", "controls.app: app_fg_bg is a required field") args.height = 4 - args.width = 5 + args.width = 5 -- create new graphics element base object local e = element.new(args) - -- write app title, centered - e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4) - e.w_write(args.title) - -- draw the app button local function draw() local fgd = args.app_fg_bg.fgd @@ -120,8 +116,18 @@ local function app_button(args) if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end end + -- element redraw + function e.redraw() + -- write app title, centered + e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4) + e.w_write(args.title) + + -- draw button + draw() + end + -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/checkbox.lua index 663e18d..a31e636 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -18,8 +18,8 @@ local element = require("graphics.element") ---@param args checkbox_args ---@return graphics_element element, element_id id local function checkbox(args) - assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field") - assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field") + assert(type(args.label) == "string", "controls.checkbox: label is a required field") + assert(type(args.box_fg_bg) == "table", "controls.checkbox: box_fg_bg is a required field") args.can_focus = true args.height = 1 @@ -105,9 +105,14 @@ local function checkbox(args) e.on_enabled = draw_label e.on_disabled = draw_label + -- element redraw + function e.redraw() + draw() + draw_label() + end + -- initial draw - draw() - draw_label() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index 92e4ac5..9f402cf 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -21,21 +21,16 @@ local element = require("graphics.element") ---@param args hazard_button_args ---@return graphics_element element, element_id id local function hazard_button(args) - assert(type(args.text) == "string", "graphics.elements.controls.hazard_button: text is a required field") - assert(type(args.accent) == "number", "graphics.elements.controls.hazard_button: accent is a required field") - assert(type(args.callback) == "function", "graphics.elements.controls.hazard_button: callback is a required field") + assert(type(args.text) == "string", "controls.hazard_button: text is a required field") + assert(type(args.accent) == "number", "controls.hazard_button: accent is a required field") + assert(type(args.callback) == "function", "controls.hazard_button: callback is a required field") - -- static dimensions args.height = 3 args.width = string.len(args.text) + 4 -- create new graphics element base object local e = element.new(args) - -- write the button text - e.w_set_cur(3, 2) - e.w_write(args.text) - -- draw border ---@param accent color accent color local function draw_border(accent) @@ -158,7 +153,6 @@ local function hazard_button(args) -- 1.5 second timeout tcd.dispatch(1.5, on_timeout) - -- call the touch callback args.callback() end end @@ -195,8 +189,16 @@ local function hazard_button(args) e.w_write(args.text) end - -- initial draw of border - draw_border(args.accent) + -- element redraw + function e.redraw() + -- write the button text and draw border + e.w_set_cur(3, 2) + e.w_write(args.text) + draw_border(args.accent) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua index f6116b0..4de45b0 100644 --- a/graphics/elements/controls/multi_button.lua +++ b/graphics/elements/controls/multi_button.lua @@ -29,13 +29,11 @@ local element = require("graphics.element") ---@param args multi_button_args ---@return graphics_element element, element_id id local function multi_button(args) - assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field") - assert(#args.options > 0, "graphics.elements.controls.multi_button: at least one option is required") - assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field") - assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), - "graphics.elements.controls.multi_button: default must be nil or a number > 0") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), - "graphics.elements.controls.multi_button: min_width must be nil or a number > 0") + assert(type(args.options) == "table", "controls.multi_button: options is a required field") + assert(#args.options > 0, "controls.multi_button: at least one option is required") + assert(type(args.callback) == "function", "controls.multi_button: callback is a required field") + assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "controls.multi_button: default must be nil or a number > 0") + assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.multi_button: min_width must be nil or a number > 0") -- single line args.height = 1 @@ -71,7 +69,7 @@ local function multi_button(args) end -- show the button state - local function draw() + function e.redraw() for i = 1, #args.options do local opt = args.options[i] ---@type button_option @@ -115,7 +113,7 @@ local function multi_button(args) -- tap always has identical coordinates, so this always passes for taps if button_ini == button_cur and button_cur ~= nil then e.value = button_cur - draw() + e.redraw() args.callback(e.value) end end @@ -125,11 +123,11 @@ local function multi_button(args) ---@param val integer new value function e.set_value(val) e.value = val - draw() + e.redraw() end -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index cab0977..02bda8a 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -26,10 +26,9 @@ local KEY_CLICK = core.events.KEY_CLICK ---@param args push_button_args ---@return graphics_element element, element_id id local function push_button(args) - assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field") - assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), - "graphics.elements.controls.push_button: min_width must be nil or a number > 0") + assert(type(args.text) == "string", "controls.push_button: text is a required field") + assert(type(args.callback) == "function", "controls.push_button: callback is a required field") + assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.push_button: min_width must be nil or a number > 0") local text_width = string.len(args.text) @@ -46,7 +45,7 @@ local function push_button(args) local v_pad = math.floor(e.frame.h / 2) + 1 -- draw the button - local function draw() + function e.redraw() e.window.clear() -- write the button text @@ -60,7 +59,7 @@ local function push_button(args) e.value = true e.w_set_fgd(args.active_fg_bg.fgd) e.w_set_bkg(args.active_fg_bg.bkg) - draw() + e.redraw() end end @@ -70,7 +69,7 @@ local function push_button(args) e.value = false e.w_set_fgd(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.bkg) - draw() + e.redraw() end end @@ -117,7 +116,7 @@ local function push_button(args) e.value = false e.w_set_fgd(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.bkg) - draw() + e.redraw() end end @@ -127,7 +126,7 @@ local function push_button(args) e.value = false e.w_set_fgd(args.dis_fg_bg.fgd) e.w_set_bkg(args.dis_fg_bg.bkg) - draw() + e.redraw() end end @@ -136,7 +135,7 @@ local function push_button(args) e.on_unfocused = show_unpressed -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/radio_2d.lua index a3ac712..e4386f1 100644 --- a/graphics/elements/controls/radio_2d.lua +++ b/graphics/elements/controls/radio_2d.lua @@ -27,11 +27,12 @@ local element = require("graphics.element") ---@param args radio_2d_args ---@return graphics_element element, element_id id local function radio_2d_button(args) - assert(type(args.options) == "table" and #args.options > 0, "graphics.elements.controls.radio_2d: options should be a table with length >= 1") - assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_2d: radio_colors is a required field") - assert(type(args.select_color) == "number" or type(args.color_map) == "table", "graphics.elements.controls.radio_2d: select_color or color_map is required") - assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), - "graphics.elements.controls.radio_2d: default must be nil or a number > 0") + assert(type(args.options) == "table" and #args.options > 0, "controls.radio_2d: options should be a table with length >= 1") + assert(util.is_int(args.rows) and util.is_int(args.columns), "controls.radio_2d: rows/columns must be integers") + assert((args.rows * args.columns) >= #args.options, "controls.radio_2d: rows x columns size insufficient for provided number of options") + assert(type(args.radio_colors) == "table", "controls.radio_2d: radio_colors is a required field") + assert(type(args.select_color) == "number" or type(args.color_map) == "table", "controls.radio_2d: select_color or color_map is required") + assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "controls.radio_2d: default must be nil or a number > 0") local array = {} local col_widths = {} @@ -74,8 +75,8 @@ local function radio_2d_button(args) -- selected option (convert nil to 1 if missing) e.value = args.default or 1 - -- show the args.options/states - local function draw() + -- draw the element + function e.redraw() local col_x = 1 local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b) @@ -135,7 +136,7 @@ local function radio_2d_button(args) if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then e.value = elem.id focused_opt = elem.id - draw() + e.redraw() if type(args.callback) == "function" then args.callback(e.value) end break end @@ -149,30 +150,30 @@ local function radio_2d_button(args) if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then e.value = focused_opt - draw() + e.redraw() if type(args.callback) == "function" then args.callback(e.value) end elseif event.key == keys.down then if focused_opt < #args.options then focused_opt = focused_opt + 1 - draw() + e.redraw() end elseif event.key == keys.up then if focused_opt > 1 then focused_opt = focused_opt - 1 - draw() + e.redraw() end elseif event.key == keys.right then if array[focus_y + 1] and array[focus_y + 1][focus_x] then focused_opt = array[focus_y + 1][focus_x].id else focused_opt = array[1][focus_x].id end - draw() + e.redraw() elseif event.key == keys.left then if array[focus_y - 1] and array[focus_y - 1][focus_x] then focused_opt = array[focus_y - 1][focus_x].id - draw() + e.redraw() elseif array[#array][focus_x] then focused_opt = array[#array][focus_x].id - draw() + e.redraw() end end end @@ -183,20 +184,20 @@ local function radio_2d_button(args) function e.set_value(val) if val > 0 and val <= #args.options then e.value = val - draw() + e.redraw() end end -- handle focus - e.on_focused = draw - e.on_unfocused = draw + e.on_focused = e.redraw + e.on_unfocused = e.redraw -- handle enable - e.on_enabled = draw - e.on_disabled = draw + e.on_enabled = e.redraw + e.on_disabled = e.redraw -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index cff16eb..483f1f8 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -25,14 +25,14 @@ local KEY_CLICK = core.events.KEY_CLICK ---@param args radio_button_args ---@return graphics_element element, element_id id local function radio_button(args) - assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field") - assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required") - assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_button: radio_colors is a required field") - assert(type(args.select_color) == "number", "graphics.elements.controls.radio_button: select_color is a required field") + assert(type(args.options) == "table", "controls.radio_button: options is a required field") + assert(#args.options > 0, "controls.radio_button: at least one option is required") + assert(type(args.radio_colors) == "table", "controls.radio_button: radio_colors is a required field") + assert(type(args.select_color) == "number", "controls.radio_button: select_color is a required field") assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), - "graphics.elements.controls.radio_button: default must be nil or a number > 0") + "controls.radio_button: default must be nil or a number > 0") assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), - "graphics.elements.controls.radio_button: min_width must be nil or a number > 0") + "controls.radio_button: min_width must be nil or a number > 0") -- determine widths local max_width = 1 @@ -59,7 +59,7 @@ local function radio_button(args) e.value = args.default or 1 -- show the button state - local function draw() + function e.redraw() for i = 1, #args.options do local opt = args.options[i] ---@type string @@ -97,7 +97,7 @@ local function radio_button(args) if args.options[event.current.y] ~= nil then e.value = event.current.y focused_opt = e.value - draw() + e.redraw() if type(args.callback) == "function" then args.callback(e.value) end end end @@ -109,17 +109,17 @@ local function radio_button(args) if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then e.value = focused_opt - draw() + e.redraw() if type(args.callback) == "function" then args.callback(e.value) end elseif event.key == keys.down then if focused_opt < #args.options then focused_opt = focused_opt + 1 - draw() + e.redraw() end elseif event.key == keys.up then if focused_opt > 1 then focused_opt = focused_opt - 1 - draw() + e.redraw() end end end @@ -130,20 +130,20 @@ local function radio_button(args) function e.set_value(val) if val > 0 and val <= #args.options then e.value = val - draw() + e.redraw() end end -- handle focus - e.on_focused = draw - e.on_unfocused = draw + e.on_focused = e.redraw + e.on_unfocused = e.redraw -- handle enable - e.on_enabled = draw - e.on_disabled = draw + e.on_enabled = e.redraw + e.on_disabled = e.redraw -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua index 646c724..3ac6bb1 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/sidebar.lua @@ -1,6 +1,7 @@ -- Sidebar Graphics Element local tcd = require("scada-common.tcd") +local util = require("scada-common.util") local core = require("graphics.core") local element = require("graphics.element") @@ -26,11 +27,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@param args sidebar_args ---@return graphics_element element, element_id id local function sidebar(args) - assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field") - assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required") - assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field") + assert(type(args.tabs) == "table", "controls.sidebar: tabs is a required field") + assert(#args.tabs > 0, "controls.sidebar: at least one tab is required") + assert(type(args.callback) == "function", "controls.sidebar: callback is a required field") - -- always 3 wide args.width = 3 -- create new graphics element base object @@ -41,10 +41,14 @@ local function sidebar(args) -- default to 1st tab e.value = 1 + local was_pressed = false + -- show the button state - ---@param pressed boolean if the currently selected tab should appear as actively pressed + ---@param pressed? boolean if the currently selected tab should appear as actively pressed ---@param pressed_idx? integer optional index to show as held (that is not yet selected) local function draw(pressed, pressed_idx) + pressed = util.trinary(pressed == nil, was_pressed, pressed) + was_pressed = pressed pressed_idx = pressed_idx or e.value for i = 1, #args.tabs do @@ -65,12 +69,8 @@ local function sidebar(args) e.w_write(" ") e.w_set_cur(1, y + 1) if e.value == i then - -- show as selected e.w_write(" " .. tab.char .. "\x10") - else - -- show as unselected - e.w_write(" " .. tab.char .. " ") - end + else e.w_write(" " .. tab.char .. " ") end e.w_set_cur(1, y + 2) e.w_write(" ") end @@ -113,8 +113,10 @@ local function sidebar(args) draw(false) end - -- initial draw - draw(false) + -- element redraw + e.redraw = draw + + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua index f7d0ee5..7615ecf 100644 --- a/graphics/elements/controls/spinbox_numeric.lua +++ b/graphics/elements/controls/spinbox_numeric.lua @@ -29,8 +29,8 @@ local function spinbox(args) local wn_prec = args.whole_num_precision local fr_prec = args.fractional_precision - assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer") - assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer") + assert(util.is_int(wn_prec), "controls.spinbox_numeric: whole number precision must be an integer") + assert(util.is_int(fr_prec), "controls.spinbox_numeric: fractional precision must be an integer") local fmt, fmt_init ---@type string, string @@ -44,7 +44,7 @@ local function spinbox(args) local dec_point_x = args.whole_num_precision + 1 - assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field") + assert(type(args.arrow_fg_bg) == "table", "controls.spinbox_numeric: arrow_fg_bg is a required field") -- determine widths args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0) @@ -72,8 +72,6 @@ local function spinbox(args) end end - draw_arrows(args.arrow_fg_bg.fgd) - -- populate digits from current value local function set_digits() local initial_str = util.sprintf(fmt_init, e.value) @@ -125,9 +123,6 @@ local function spinbox(args) e.w_write(util.sprintf(fmt, e.value)) end - -- init with the default value - show_num() - -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) @@ -138,10 +133,8 @@ local function spinbox(args) local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x) if digits[idx] ~= nil then if event.current.y == 1 then - -- increment digits[idx] = digits[idx] + 1 elseif event.current.y == 3 then - -- decrement digits[idx] = digits[idx] - 1 end @@ -176,18 +169,19 @@ local function spinbox(args) end -- enable this input - function e.on_enabled() - draw_arrows(args.arrow_fg_bg.fgd) - end + function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end -- disable this input - function e.on_disabled() - draw_arrows(args.arrow_disable or colors.lightGray) + function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end + + -- element redraw + function e.redraw() + show_num() + draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray)) end - -- default to zero, init digits table - e.value = 0 - set_digits() + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua index 63f6c9b..d8da813 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -21,15 +21,13 @@ local element = require("graphics.element") ---@param args switch_button_args ---@return graphics_element element, element_id id local function switch_button(args) - assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field") - assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field") - assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), - "graphics.elements.controls.switch_button: min_width must be nil or a number > 0") + assert(type(args.text) == "string", "controls.switch_button: text is a required field") + assert(type(args.callback) == "function", "controls.switch_button: callback is a required field") + assert(type(args.active_fg_bg) == "table", "controls.switch_button: active_fg_bg is a required field") + assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.switch_button: min_width must be nil or a number > 0") local text_width = string.len(args.text) - -- single line height, calculate width args.height = 1 args.min_width = args.min_width or 0 args.width = math.max(text_width, args.min_width) @@ -37,44 +35,32 @@ local function switch_button(args) -- create new graphics element base object local e = element.new(args) - -- button state (convert nil to false if missing) e.value = args.default or false local h_pad = math.floor((e.frame.w - text_width) / 2) + 1 local v_pad = math.floor(e.frame.h / 2) + 1 -- show the button state - local function draw_state() + function e.redraw() if e.value then - -- show as pressed e.w_set_fgd(args.active_fg_bg.fgd) e.w_set_bkg(args.active_fg_bg.bkg) else - -- show as unpressed e.w_set_fgd(e.fg_bg.fgd) e.w_set_bkg(e.fg_bg.bkg) end - -- clear to redraw background e.window.clear() - - -- write the button text e.w_set_cur(h_pad, v_pad) e.w_write(args.text) end - -- initial draw - draw_state() - -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) if e.enabled and core.events.was_clicked(event.type) then - -- toggle state e.value = not e.value - draw_state() - - -- call the touch callback with state + e.redraw() args.callback(e.value) end end @@ -82,11 +68,13 @@ local function switch_button(args) -- set the value ---@param val boolean new value function e.set_value(val) - -- set state e.value = val - draw_state() + e.redraw() end + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/controls/tabbar.lua b/graphics/elements/controls/tabbar.lua index e1a99ca..ad2c0d7 100644 --- a/graphics/elements/controls/tabbar.lua +++ b/graphics/elements/controls/tabbar.lua @@ -27,13 +27,11 @@ local element = require("graphics.element") ---@param args tabbar_args ---@return graphics_element element, element_id id local function tabbar(args) - assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field") - assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required") - assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), - "graphics.elements.controls.tabbar: min_width must be nil or a number > 0") + assert(type(args.tabs) == "table", "controls.tabbar: tabs is a required field") + assert(#args.tabs > 0, "controls.tabbar: at least one tab is required") + assert(type(args.callback) == "function", "controls.tabbar: callback is a required field") + assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.tabbar: min_width must be nil or a number > 0") - -- always 1 tall args.height = 1 -- determine widths @@ -67,7 +65,7 @@ local function tabbar(args) end -- show the tab state - local function draw() + function e.redraw() for i = 1, #args.tabs do local tab = args.tabs[i] ---@type tabbar_tab @@ -109,7 +107,7 @@ local function tabbar(args) -- tap always has identical coordinates, so this always passes for taps if tab_ini == tab_cur and tab_cur ~= nil then e.value = tab_cur - draw() + e.redraw() args.callback(e.value) end end @@ -119,11 +117,11 @@ local function tabbar(args) ---@param val integer new value function e.set_value(val) e.value = val - draw() + e.redraw() end -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index 3e2d350..f444073 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -42,30 +42,6 @@ local function number_field(args) -- make an interactive field manager local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg) - - -- draw input - local function show() - if e.enabled then - e.w_set_bkg(args.fg_bg.bkg) - e.w_set_fgd(args.fg_bg.fgd) - else - e.w_set_bkg(args.dis_fg_bg.bkg) - e.w_set_fgd(args.dis_fg_bg.fgd) - end - - -- clear and print - e.w_set_cur(1, 1) - e.w_write(string.rep(" ", e.frame.w)) - e.w_set_cur(1, 1) - e.w_write(e.value) - - -- show cursor if focused - if e.is_focused() and e.enabled then - e.w_set_fgd(colors.lightGray) - e.w_write("_") - end - end - -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) @@ -117,9 +93,7 @@ local function number_field(args) -- set the value (must be a number) ---@param val number number to show function e.set_value(val) - if tonumber(val) then - ifield.set_value("" .. tonumber(val)) - end + if tonumber(val) then ifield.set_value("" .. tonumber(val)) end end -- set minimum input value @@ -140,9 +114,6 @@ local function number_field(args) end end - -- handle focused - e.on_focused = show - -- handle unfocused function e.on_unfocused() local val = tonumber(e.value) @@ -162,12 +133,14 @@ local function number_field(args) ifield.show() end - -- handle enable + -- handle focus (not unfocus), enable, and redraw with show() + e.on_focused = ifield.show e.on_enabled = ifield.show e.on_disabled = ifield.show + e.redraw = ifield.show -- initial draw - ifield.show() + e.redraw() return e.complete() end diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 3211676..5fc1062 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -88,16 +88,15 @@ local function text_field(args) ifield.set_value(text) end - -- handle focus + -- handle focus, enable, and redraw with show() e.on_focused = ifield.show e.on_unfocused = ifield.show - - -- handle enable e.on_enabled = ifield.show e.on_disabled = ifield.show + e.redraw = ifield.show -- initial draw - ifield.show() + e.redraw() local elem, id = e.complete() return elem, id, ifield.censor diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua index 659b216..8ed5337 100644 --- a/graphics/elements/indicators/alight.lua +++ b/graphics/elements/indicators/alight.lua @@ -25,13 +25,13 @@ local flasher = require("graphics.flasher") ---@param args alarm_indicator_light ---@return graphics_element element, element_id id local function alarm_indicator_light(args) - assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field") - assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field") - assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field") - assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field") + assert(type(args.label) == "string", "indicators.alight: label is a required field") + assert(type(args.c1) == "number", "indicators.alight: c1 is a required field") + assert(type(args.c2) == "number", "indicators.alight: c2 is a required field") + assert(type(args.c3) == "number", "indicators.alight: c3 is a required field") if args.flash then - assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled") + assert(util.is_int(args.period), "indicators.alight: period is a required field if flash is enabled") end -- single line @@ -51,6 +51,8 @@ local function alarm_indicator_light(args) -- create new graphics element base object local e = element.new(args) + e.value = 1 + -- called by flasher when enabled local function flash_callback() e.w_set_cur(1, 1) @@ -105,9 +107,14 @@ local function alarm_indicator_light(args) ---@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.w_write(args.label) + -- draw label and indicator light + function e.redraw() + e.on_update(e.value) + e.w_write(args.label) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/coremap.lua index 4140c5f..12319f6 100644 --- a/graphics/elements/indicators/coremap.lua +++ b/graphics/elements/indicators/coremap.lua @@ -18,8 +18,8 @@ local element = require("graphics.element") ---@param args core_map_args ---@return graphics_element element, element_id id local function core_map(args) - assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field") - assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field") + assert(util.is_int(args.reactor_l), "indicators.coremap: reactor_l is a required field") + assert(util.is_int(args.reactor_w), "indicators.coremap: reactor_w is a required field") -- require max dimensions args.width = 18 @@ -31,6 +31,8 @@ local function core_map(args) -- create new graphics element base object local e = element.new(args) + e.value = 0 + local alternator = true local core_l = args.reactor_l - 2 @@ -157,11 +159,14 @@ local function core_map(args) e.on_update(e.value) end - -- initial (one-time except for resize()) frame draw - draw_frame() + -- redraw both frame and core + function e.redraw() + draw_frame() + draw_core(e.value) + end -- initial draw - e.on_update(0) + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/data.lua index 9d7f435..6097047 100644 --- a/graphics/elements/indicators/data.lua +++ b/graphics/elements/indicators/data.lua @@ -24,25 +24,17 @@ local element = require("graphics.element") ---@param args data_indicator_args ---@return graphics_element element, element_id id local function data(args) - assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field") - assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field") - assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field") - assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field") + assert(type(args.label) == "string", "indicators.data: label is a required field") + assert(type(args.format) == "string", "indicators.data: format is a required field") + assert(args.value ~= nil, "indicators.data: value is a required field") + assert(util.is_int(args.width), "indicators.data: width is a required field") - -- single line args.height = 1 -- create new graphics element base object local e = element.new(args) - -- label color - if args.lu_colors ~= nil then - e.w_set_fgd(args.lu_colors.color_a) - end - - -- write label - e.w_set_cur(1, 1) - e.w_write(args.label) + e.value = args.value local value_color = e.fg_bg.fgd local label_len = string.len(args.label) @@ -93,8 +85,17 @@ local function data(args) e.on_update(e.value) end - -- initial value draw - e.on_update(args.value) + -- element redraw + function e.redraw() + if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end + e.w_set_cur(1, 1) + e.w_write(args.label) + + e.on_update(e.value) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/hbar.lua index 32ab3a0..ef68dc6 100644 --- a/graphics/elements/indicators/hbar.lua +++ b/graphics/elements/indicators/hbar.lua @@ -28,10 +28,12 @@ local function hbar(args) -- create new graphics element base object local e = element.new(args) + e.value = 0.0 + -- bar width is width - 5 characters for " 100%" if showing percent local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w) - assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar") + assert(bar_width > 0, "indicators.hbar: too small for bar") -- determine bar colors local bar_bkg = e.fg_bg.blit_bkg @@ -105,20 +107,21 @@ local function hbar(args) function e.recolor(bar_fg_bg) bar_bkg = bar_fg_bg.blit_bkg bar_fgd = bar_fg_bg.blit_fgd - - -- re-draw - last_num_bars = 0 - if type(e.value) == "number" then - e.on_update(e.value) - end + e.redraw() end -- set the percentage value ---@param val number 0.0 to 1.0 function e.set_value(val) e.on_update(val) end - -- initialize to 0 - e.on_update(0) + -- element redraw + function e.redraw() + last_num_bars = -1 + e.on_update(e.value) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua index a3561de..70d7d2c 100644 --- a/graphics/elements/indicators/icon.lua +++ b/graphics/elements/indicators/icon.lua @@ -23,18 +23,17 @@ local element = require("graphics.element") ---@param args icon_indicator_args ---@return graphics_element element, element_id id local function icon(args) - assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field") - assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field") + assert(type(args.label) == "string", "indicators.icon: label is a required field") + assert(type(args.states) == "table", "indicators.icon: states 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)) + 4 -- create new graphics element base object local e = element.new(args) + e.value = args.value or 1 + -- state blit strings local state_blit_cmds = {} for i = 1, #args.states do @@ -47,10 +46,6 @@ local function icon(args) }) end - -- write label and initial indicator light - e.w_set_cur(5, 1) - e.w_write(args.label) - -- on state change ---@param new_state integer indicator state function e.on_update(new_state) @@ -64,8 +59,16 @@ local function icon(args) ---@param val integer indicator state function e.set_value(val) e.on_update(val) end - -- initial icon draw - e.on_update(args.value or 1) + -- element redraw + function e.redraw() + e.w_set_cur(5, 1) + e.w_write(args.label) + + e.on_update(e.value) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua index 53297ec..72e1e93 100644 --- a/graphics/elements/indicators/led.lua +++ b/graphics/elements/indicators/led.lua @@ -23,25 +23,23 @@ local flasher = require("graphics.flasher") ---@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") + assert(type(args.label) == "string", "indicators.led: label is a required field") + assert(type(args.colors) == "table", "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") + assert(util.is_int(args.period), "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 0, string.len(args.label)) + 2 - -- flasher state local flash_on = true -- create new graphics element base object local e = element.new(args) + e.value = false + -- called by flasher when enabled local function flash_callback() e.w_set_cur(1, 1) @@ -88,13 +86,18 @@ local function indicator_led(args) ---@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) - if string.len(args.label) > 0 then - e.w_set_cur(3, 1) - e.w_write(args.label) + -- draw label and indicator light + function e.redraw() + e.on_update(e.value) + if string.len(args.label) > 0 then + e.w_set_cur(3, 1) + e.w_write(args.label) + end end + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua index fed141f..3ee99fb 100644 --- a/graphics/elements/indicators/ledpair.lua +++ b/graphics/elements/indicators/ledpair.lua @@ -25,25 +25,20 @@ local flasher = require("graphics.flasher") ---@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") + assert(type(args.label) == "string", "indicators.ledpair: label is a required field") + assert(type(args.off) == "number", "indicators.ledpair: off is a required field") + assert(type(args.c1) == "number", "indicators.ledpair: c1 is a required field") + assert(type(args.c2) == "number", "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") + assert(util.is_int(args.period), "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 0, 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) @@ -51,7 +46,6 @@ local function indicator_led_pair(args) -- 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 @@ -86,7 +80,6 @@ local function indicator_led_pair(args) elseif new_state <= 1 then flash_on = false flasher.stop(flash_callback) - e.w_blit("\x8c", co, e.fg_bg.blit_bkg) end elseif new_state == 2 then @@ -102,13 +95,18 @@ local function indicator_led_pair(args) ---@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) - if string.len(args.label) > 0 then - e.w_set_cur(3, 1) - e.w_write(args.label) + -- draw label and indicator light + function e.redraw() + e.on_update(e.value) + if string.len(args.label) > 0 then + e.w_set_cur(3, 1) + e.w_write(args.label) + end end + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua index 79064b4..f2320c6 100644 --- a/graphics/elements/indicators/ledrgb.lua +++ b/graphics/elements/indicators/ledrgb.lua @@ -18,19 +18,15 @@ local element = require("graphics.element") ---@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") + assert(type(args.label) == "string", "indicators.ledrgb: label is a required field") + assert(type(args.colors) == "table", "indicators.ledrgb: colors is a required field") - -- single line args.height = 1 - - -- determine width 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) - -- init value for initial check in on_update e.value = 1 -- on state change @@ -47,13 +43,18 @@ local function indicator_led_rgb(args) ---@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) - if string.len(args.label) > 0 then - e.w_set_cur(3, 1) - e.w_write(args.label) + -- draw label and indicator light + function e.redraw() + e.on_update(e.value) + if string.len(args.label) > 0 then + e.w_set_cur(3, 1) + e.w_write(args.label) + end end + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua index cea5916..742d6c1 100644 --- a/graphics/elements/indicators/light.lua +++ b/graphics/elements/indicators/light.lua @@ -23,25 +23,23 @@ local flasher = require("graphics.flasher") ---@param args indicator_light_args ---@return graphics_element element, element_id id local function indicator_light(args) - assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field") - assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field") + assert(type(args.label) == "string", "indicators.light: label is a required field") + assert(type(args.colors) == "table", "indicators.light: colors is a required field") if args.flash then - assert(util.is_int(args.period), "graphics.elements.indicators.light: period is a required field if flash is enabled") + assert(util.is_int(args.period), "indicators.light: 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) + e.value = false + -- called by flasher when enabled local function flash_callback() e.w_set_cur(1, 1) @@ -88,10 +86,15 @@ local function indicator_light(args) ---@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.w_set_cur(3, 1) - e.w_write(args.label) + -- draw label and indicator light + function e.redraw() + e.on_update(false) + e.w_set_cur(3, 1) + e.w_write(args.label) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua index e694a57..eaa1f68 100644 --- a/graphics/elements/indicators/power.lua +++ b/graphics/elements/indicators/power.lua @@ -9,7 +9,7 @@ local element = require("graphics.element") ---@field format string power format override (lua string format) ---@field rate boolean? whether to append /t to the end (power per tick) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ----@field value any default value +---@field value number default value ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -23,26 +23,17 @@ local element = require("graphics.element") ---@param args power_indicator_args ---@return graphics_element element, element_id id local function power(args) - assert(args.value ~= nil, "graphics.elements.indicators.power: value is a required field") - assert(util.is_int(args.width), "graphics.elements.indicators.power: width is a required field") + assert(type(args.value) == "number", "indicators.power: value is a required number field") + assert(util.is_int(args.width), "indicators.power: width is a required field") - -- single line args.height = 1 -- create new graphics element base object local e = element.new(args) - -- label color - if args.lu_colors ~= nil then - e.w_set_fgd(args.lu_colors.color_a) - end + e.value = args.value - -- write label - e.w_set_cur(1, 1) - e.w_write(args.label) - - local data_start = string.len(args.label) + 2 - if string.len(args.label) == 0 then data_start = 1 end + local data_start = 0 -- on state change ---@param value any new value @@ -77,8 +68,20 @@ local function power(args) ---@param val any new value function e.set_value(val) e.on_update(val) end - -- initial value draw - e.on_update(args.value) + -- element redraw + function e.redraw() + if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end + e.w_set_cur(1, 1) + e.w_write(args.label) + + data_start = string.len(args.label) + 2 + if string.len(args.label) == 0 then data_start = 1 end + + e.on_update(e.value) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua index c110f22..2ae9b6f 100644 --- a/graphics/elements/indicators/rad.lua +++ b/graphics/elements/indicators/rad.lua @@ -10,7 +10,7 @@ local element = require("graphics.element") ---@field format string data format (lua string format) ---@field commas? boolean whether to use commas if a number is given (default to false) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ----@field value any default value +---@field value number default value ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -24,24 +24,17 @@ local element = require("graphics.element") ---@param args rad_indicator_args ---@return graphics_element element, element_id id local function rad(args) - assert(type(args.label) == "string", "graphics.elements.indicators.rad: label is a required field") - assert(type(args.format) == "string", "graphics.elements.indicators.rad: format is a required field") - assert(util.is_int(args.width), "graphics.elements.indicators.rad: width is a required field") + assert(type(args.value) ~= "number", "indicators.rad: value is a required number field") + assert(type(args.label) == "string", "indicators.rad: label is a required field") + assert(type(args.format) == "string", "indicators.rad: format is a required field") + assert(util.is_int(args.width), "indicators.rad: width is a required field") - -- single line args.height = 1 -- create new graphics element base object local e = element.new(args) - -- label color - if args.lu_colors ~= nil then - e.w_set_fgd(args.lu_colors.color_a) - end - - -- write label - e.w_set_cur(1, 1) - e.w_write(args.label) + e.value = types.new_zero_radiation_reading() local label_len = string.len(args.label) local data_start = 1 @@ -82,8 +75,17 @@ local function rad(args) ---@param val any new value function e.set_value(val) e.on_update(val) end - -- initial value draw - e.on_update(types.new_zero_radiation_reading()) + -- element redraw + function e.redraw() + if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end + e.w_set_cur(1, 1) + e.w_write(args.label) + + e.on_update(e.value) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/state.lua index dfd6e0b..2096c6b 100644 --- a/graphics/elements/indicators/state.lua +++ b/graphics/elements/indicators/state.lua @@ -25,16 +25,14 @@ local element = require("graphics.element") ---@param args state_indicator_args ---@return graphics_element element, element_id id local function state_indicator(args) - assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field") + assert(type(args.states) == "table", "indicators.state: states is a required field") - -- determine height if util.is_int(args.height) then - assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number") + assert(args.height % 2 == 1, "indicators.state: height should be an odd number") else args.height = 1 end - -- initial guess at width args.width = args.min_width or 1 -- state blit strings @@ -42,7 +40,6 @@ local function state_indicator(args) for i = 1, #args.states do local state_def = args.states[i] ---@type state_text_color - -- re-determine width if string.len(state_def.text) > args.width then args.width = string.len(state_def.text) end @@ -59,13 +56,20 @@ local function state_indicator(args) -- create new graphics element base object local e = element.new(args) + e.value = args.value or 1 + + -- element redraw + function e.redraw() + local blit_cmd = state_blit_cmds[e.value] + e.w_set_cur(1, 1) + e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) + end + -- on state change ---@param new_state integer indicator state function e.on_update(new_state) - local blit_cmd = state_blit_cmds[new_state] e.value = new_state - e.w_set_cur(1, 1) - e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) + e.redraw() end -- set indicator state @@ -73,7 +77,7 @@ local function state_indicator(args) function e.set_value(val) e.on_update(val) end -- initial draw - e.on_update(args.value or 1) + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua index 9319cea..f90b21b 100644 --- a/graphics/elements/indicators/trilight.lua +++ b/graphics/elements/indicators/trilight.lua @@ -25,35 +25,29 @@ local flasher = require("graphics.flasher") ---@param args tristate_indicator_light_args ---@return graphics_element element, element_id id local function tristate_indicator_light(args) - assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field") - assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field") - assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field") - assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field") + assert(type(args.label) == "string", "indicators.trilight: label is a required field") + assert(type(args.c1) == "number", "indicators.trilight: c1 is a required field") + assert(type(args.c2) == "number", "indicators.trilight: c2 is a required field") + assert(type(args.c3) == "number", "indicators.trilight: c3 is a required field") if args.flash then - assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled") + assert(util.is_int(args.period), "indicators.trilight: 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 c1 = colors.toBlit(args.c1) - local c2 = colors.toBlit(args.c2) - local c3 = colors.toBlit(args.c3) - -- create new graphics element base object local e = element.new(args) - -- init value for initial check in on_update e.value = 1 + local flash_on = true + + local c1 = colors.toBlit(args.c1) + local c2 = colors.toBlit(args.c2) + local c3 = colors.toBlit(args.c3) + -- called by flasher when enabled local function flash_callback() e.w_set_cur(1, 1) @@ -102,9 +96,14 @@ local function tristate_indicator_light(args) ---@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.w_write(args.label) + -- draw light and label + function e.redraw() + e.on_update(1) + e.w_write(args.label) + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/indicators/vbar.lua b/graphics/elements/indicators/vbar.lua index 05aecf3..afe56fc 100644 --- a/graphics/elements/indicators/vbar.lua +++ b/graphics/elements/indicators/vbar.lua @@ -20,13 +20,13 @@ local element = require("graphics.element") ---@param args vbar_args ---@return graphics_element element, element_id id local function vbar(args) - -- properties/state - local last_num_bars = -1 - -- create new graphics element base object local e = element.new(args) - -- blit strings + e.value = 0.0 + + local last_num_bars = -1 + local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w) local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w) local spaces = util.spaces(e.frame.w) @@ -52,10 +52,7 @@ local function vbar(args) if num_bars ~= last_num_bars then last_num_bars = num_bars - -- start bottom up local y = e.frame.h - - -- start at base of vertical bar e.w_set_cur(1, y) -- fill percentage @@ -83,22 +80,26 @@ local function vbar(args) end end + -- set the percentage value + ---@param val number 0.0 to 1.0 + function e.set_value(val) e.on_update(val) end + + -- element redraw + function e.redraw() + last_num_bars = -1 + e.on_update(e.value) + end + -- change bar color ---@param fg_bg cpair new bar colors function e.recolor(fg_bg) fgd = string.rep(fg_bg.blit_fgd, e.frame.w) bkg = string.rep(fg_bg.blit_bkg, e.frame.w) - - -- re-draw - last_num_bars = 0 - if type(e.value) == "number" then - e.on_update(e.value) - end + e.redraw() end - -- set the percentage value - ---@param val number 0.0 to 1.0 - function e.set_value(val) e.on_update(val) end + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index 0268951..a35330a 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/listbox.lua @@ -274,8 +274,20 @@ local function listbox(args) end end - draw_arrows(0) - draw_bar() + -- element redraw + function e.redraw() + draw_arrows(0) + draw_bar() + + -- redraw all children + for i = 1, #list do + local item = list[i] ---@type listbox_item + item.e.redraw() + end + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/multipane.lua b/graphics/elements/multipane.lua index fff8fb3..5cfd217 100644 --- a/graphics/elements/multipane.lua +++ b/graphics/elements/multipane.lua @@ -19,23 +19,30 @@ local element = require("graphics.element") ---@param args multipane_args ---@return graphics_element element, element_id id local function multipane(args) - assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field") + assert(type(args.panes) == "table", "multipane: panes is a required field") -- create new graphics element base object local e = element.new(args) + e.value = 1 + + -- show the selected pane + function e.redraw() + for i = 1, #args.panes do args.panes[i].hide() end + args.panes[e.value].show() + end + -- select which pane is shown ---@param value integer pane to show function e.set_value(value) if (e.value ~= value) and (value > 0) and (value <= #args.panes) then e.value = value - - for i = 1, #args.panes do args.panes[i].hide() end - args.panes[value].show() + e.redraw() end end - e.set_value(1) + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/pipenet.lua b/graphics/elements/pipenet.lua index 65ccfb7..ce15c75 100644 --- a/graphics/elements/pipenet.lua +++ b/graphics/elements/pipenet.lua @@ -24,12 +24,11 @@ local element = require("graphics.element") ---@param args pipenet_args ---@return graphics_element element, element_id id local function pipenet(args) - assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field") + assert(type(args.pipes) == "table", "pipenet: pipes is a required field") args.width = 0 args.height = 0 - -- determine width/height for i = 1, #args.pipes do local pipe = args.pipes[i] ---@type pipe @@ -57,8 +56,8 @@ local function pipenet(args) if any_thin then break end end - if not any_thin then - -- draw all pipes + -- draw all pipes by drawing out lines + local function vector_draw() for p = 1, #args.pipes do local pipe = args.pipes[p] ---@type pipe @@ -161,11 +160,109 @@ local function pipenet(args) end end end - else - -- build map if using thin pipes, easist way to check adjacent blocks (cannot 'cheat' like with standard width) + end + + -- draw a particular map cell + ---@param map table 2D cell map + ---@param x integer x coord + ---@param y integer y coord + local function draw_map_cell(map, x, y) + local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false + local char + local invert = false + + local function check(cx, cy) + return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg) + end + + if entry.thin then + if check(x - 1, y) then -- if left + if check(x, y - 1) then -- if above + if check(x + 1, y) then -- if right + if check(x, y + 1) then -- if below + char = util.trinary(entry.atr, "\x91", "\x9d") + invert = entry.atr + else -- not below + char = util.trinary(entry.atr, "\x8e", "\x8d") + end + else -- not right + if check(x, y + 1) then -- if below + char = util.trinary(entry.atr, "\x91", "\x95") + invert = entry.atr + else -- not below + char = util.trinary(entry.atr, "\x8e", "\x85") + end + end + elseif check(x, y + 1) then-- not above, if below + if check(x + 1, y) then -- if right + char = util.trinary(entry.atr, "\x93", "\x9c") + invert = entry.atr + else -- not right + char = util.trinary(entry.atr, "\x93", "\x94") + invert = entry.atr + end + else -- not above, not below + char = "\x8c" + end + elseif check(x + 1, y) then -- not left, if right + if check(x, y - 1) then -- if above + if check(x, y + 1) then -- if below + char = util.trinary(entry.atr, "\x95", "\x9d") + invert = entry.atr + else -- not below + char = util.trinary(entry.atr, "\x8a", "\x8d") + end + else -- not above + if check(x, y + 1) then -- if below + char = util.trinary(entry.atr, "\x97", "\x9c") + invert = entry.atr + else -- not below + char = "\x8c" + end + end + else -- not left, not right + char = "\x95" + invert = entry.atr + end + else + if check(x, y - 1) then -- above + -- not below and (if left or right) + if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then + char = util.trinary(entry.atr, "\x8f", " ") + invert = not entry.atr + else -- not below w/ sides only + char = " " + invert = true + end + elseif check(x, y + 1) then -- not above, if below + -- if left or right + if (check(x - 1, y) or check(x + 1, y)) then + char = "\x83" + invert = true + else -- not left or right + char = " " + invert = true + end + else -- not above, not below + char = util.trinary(entry.atr, "\x8f", "\x83") + invert = not entry.atr + end + end + + e.w_set_cur(x, y) + + if invert then + e.w_blit(char, entry.bg, entry.fg) + else + e.w_blit(char, entry.fg, entry.bg) + end + end + + -- draw all pipes by assembling and marking up a 2D map
+ -- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes + local function map_draw() local map = {} - -- allocate map for x = 1, args.width do table.insert(map, {}) for _ = 1, args.height do table.insert(map[x], false) end @@ -215,101 +312,19 @@ local function pipenet(args) -- render for x = 1, args.width do for y = 1, args.height do - local entry = map[x][y] ---@type _pipe_map_entry|false - local char - local invert = false - - if entry ~= false then - local function check(cx, cy) - return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg) - end - - if entry.thin then - if check(x - 1, y) then -- if left - if check(x, y - 1) then -- if above - if check(x + 1, y) then -- if right - if check(x, y + 1) then -- if below - char = util.trinary(entry.atr, "\x91", "\x9d") - invert = entry.atr - else -- not below - char = util.trinary(entry.atr, "\x8e", "\x8d") - end - else -- not right - if check(x, y + 1) then -- if below - char = util.trinary(entry.atr, "\x91", "\x95") - invert = entry.atr - else -- not below - char = util.trinary(entry.atr, "\x8e", "\x85") - end - end - elseif check(x, y + 1) then-- not above, if below - if check(x + 1, y) then -- if right - char = util.trinary(entry.atr, "\x93", "\x9c") - invert = entry.atr - else -- not right - char = util.trinary(entry.atr, "\x93", "\x94") - invert = entry.atr - end - else -- not above, not below - char = "\x8c" - end - elseif check(x + 1, y) then -- not left, if right - if check(x, y - 1) then -- if above - if check(x, y + 1) then -- if below - char = util.trinary(entry.atr, "\x95", "\x9d") - invert = entry.atr - else -- not below - char = util.trinary(entry.atr, "\x8a", "\x8d") - end - else -- not above - if check(x, y + 1) then -- if below - char = util.trinary(entry.atr, "\x97", "\x9c") - invert = entry.atr - else -- not below - char = "\x8c" - end - end - else -- not left, not right - char = "\x95" - invert = entry.atr - end - else - if check(x, y - 1) then -- above - -- not below and (if left or right) - if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then - char = util.trinary(entry.atr, "\x8f", " ") - invert = not entry.atr - else -- not below w/ sides only - char = " " - invert = true - end - elseif check(x, y + 1) then -- not above, if below - -- if left or right - if (check(x - 1, y) or check(x + 1, y)) then - char = "\x83" - invert = true - else -- not left or right - char = " " - invert = true - end - else -- not above, not below - char = util.trinary(entry.atr, "\x8f", "\x83") - invert = not entry.atr - end - end - - e.w_set_cur(x, y) - - if invert then - e.w_blit(char, entry.bg, entry.fg) - else - e.w_blit(char, entry.fg, entry.bg) - end - end + if map[x][y] ~= false then draw_map_cell(map, x, y) end end end end + -- element redraw + function e.redraw() + if any_thin then map_draw() else vector_draw() end + end + + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua index 40165b0..dfd07c9 100644 --- a/graphics/elements/rectangle.lua +++ b/graphics/elements/rectangle.lua @@ -22,7 +22,7 @@ local element = require("graphics.element") ---@param args rectangle_args ---@return graphics_element element, element_id id local function rectangle(args) - assert(args.border ~= nil or args.thin ~= true, "graphics.elements.rectangle: thin requires border to be provided") + assert(args.border ~= nil or args.thin ~= true, "rectangle: thin requires border to be provided") -- if thin, then width will always need to be 1 if args.thin == true then @@ -65,8 +65,8 @@ local function rectangle(args) local inner_width = e.frame.w - width_x2 -- check dimensions - assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width") - assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height") + assert(width_x2 <= e.frame.w, "rectangle: border too thick for width") + assert(width_x2 <= e.frame.h, "rectangle: border too thick for height") -- form the basic line strings and top/bottom blit strings local spaces = util.spaces(e.frame.w) @@ -126,64 +126,69 @@ local function rectangle(args) end -- draw rectangle with borders - for y = 1, e.frame.h do - e.w_set_cur(1, y) - -- top border - if y <= border_height then - -- partial pixel fill - if args.border.even and y == border_height then - if args.thin == true then - e.w_blit(p_a, p_inv_bg, p_inv_fg) - else - local _fg = util.trinary(args.even_inner == true, string.rep(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) + function e.redraw() + for y = 1, e.frame.h do + e.w_set_cur(1, y) + -- top border + if y <= border_height then + -- partial pixel fill + if args.border.even and y == border_height then + if args.thin == true then + e.w_blit(p_a, p_inv_bg, p_inv_fg) + else + local _fg = util.trinary(args.even_inner == true, string.rep(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.w_blit(p_b, _fg, _bg) - elseif width_x2 % 3 == 2 then - e.w_blit(p_a, _fg, _bg) - else - -- skip line - e.w_blit(spaces, blit_fg, blit_bg_sides) - end - end - else - e.w_blit(spaces, blit_fg, blit_bg_top_bot) - end - -- bottom border - elseif y > (e.frame.h - border_width) then - -- partial pixel fill - if args.border.even and y == ((e.frame.h - border_width) + 1) then - if args.thin == true then - if args.even_inner == true then - e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w)) - else - e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + if width_x2 % 3 == 1 then + e.w_blit(p_b, _fg, _bg) + elseif width_x2 % 3 == 2 then + e.w_blit(p_a, _fg, _bg) + else + -- skip line + e.w_blit(spaces, blit_fg, blit_bg_sides) + end 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, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) - - if width_x2 % 3 == 1 then - e.w_blit(p_a, _fg, _bg) - elseif width_x2 % 3 == 2 then - e.w_blit(p_b, _fg, _bg) + e.w_blit(spaces, blit_fg, blit_bg_top_bot) + end + -- bottom border + elseif y > (e.frame.h - border_width) then + -- partial pixel fill + if args.border.even and y == ((e.frame.h - border_width) + 1) then + if args.thin == true then + if args.even_inner == true then + e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w)) + else + e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + end else - -- skip line - e.w_blit(spaces, blit_fg, blit_bg_sides) + local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) + local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + + if width_x2 % 3 == 1 then + e.w_blit(p_a, _fg, _bg) + elseif width_x2 % 3 == 2 then + e.w_blit(p_b, _fg, _bg) + else + -- skip line + e.w_blit(spaces, blit_fg, blit_bg_sides) + end end + else + e.w_blit(spaces, blit_fg, blit_bg_top_bot) end else - e.w_blit(spaces, blit_fg, blit_bg_top_bot) - end - else - if args.thin == true then - e.w_blit(p_s, blit_fg_sides, blit_bg_sides) - else - e.w_blit(p_s, blit_fg, blit_bg_sides) + if args.thin == true then + e.w_blit(p_s, blit_fg_sides, blit_bg_sides) + else + e.w_blit(p_s, blit_fg, blit_bg_sides) + end end end end + + -- initial draw of border + e.redraw() end return e.complete() diff --git a/graphics/elements/textbox.lua b/graphics/elements/textbox.lua index 0ef223d..cfd5585 100644 --- a/graphics/elements/textbox.lua +++ b/graphics/elements/textbox.lua @@ -24,19 +24,20 @@ local TEXT_ALIGN = core.TEXT_ALIGN ---@param args textbox_args ---@return graphics_element element, element_id id local function textbox(args) - assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field") + assert(type(args.text) == "string", "textbox: text is a required field") -- create new graphics element base object local e = element.new(args) + e.value = args.text + local alignment = args.alignment or TEXT_ALIGN.LEFT -- draw textbox + function e.redraw() + e.window.clear() - local function display_text(text) - e.value = text - - local lines = util.strwrap(text, e.frame.w) + local lines = util.strwrap(e.value, e.frame.w) for i = 1, #lines do if i > e.frame.h then break end @@ -56,15 +57,16 @@ local function textbox(args) end end - display_text(args.text) - -- set the string value and re-draw the text ---@param val string value function e.set_value(val) - e.window.clear() - display_text(val) + e.value = val + e.redraw() end + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/tiling.lua b/graphics/elements/tiling.lua index 2025903..f40fef5 100644 --- a/graphics/elements/tiling.lua +++ b/graphics/elements/tiling.lua @@ -22,13 +22,11 @@ local element = require("graphics.element") ---@param args tiling_args ---@return graphics_element element, element_id id local function tiling(args) - assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field") + assert(type(args.fill_c) == "table", "tiling: fill_c is a required field") -- create new graphics element base object local e = element.new(args) - -- draw tiling box - local fill_a = args.fill_c.blit_a local fill_b = args.fill_c.blit_b @@ -38,13 +36,9 @@ local function tiling(args) local start_y = 1 local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1)) local inner_height = e.frame.h - local alternator = true -- border if args.border_c ~= nil then - e.w_set_bkg(args.border_c) - e.window.clear() - start_x = 1 + util.trinary(even, 2, 1) start_y = 2 @@ -53,35 +47,48 @@ local function tiling(args) end -- check dimensions - assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0") - assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0") - assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width") - assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height") + assert(inner_width > 0, "tiling: inner_width <= 0") + assert(inner_height > 0, "tiling: inner_height <= 0") + assert(start_x <= inner_width, "tiling: start_x > inner_width") + assert(start_y <= inner_height, "tiling: start_y > inner_height") - -- create pattern - for y = start_y, inner_height + (start_y - 1) do - e.w_set_cur(start_x, y) - for _ = 1, inner_width do - if alternator then - if even then - e.w_blit(" ", "00", fill_a .. fill_a) - else - e.w_blit(" ", "0", fill_a) - end - else - if even then - e.w_blit(" ", "00", fill_b .. fill_b) - else - e.w_blit(" ", "0", fill_b) - end - end + -- draw tiling box + function e.redraw() + local alternator = true - alternator = not alternator + if args.border_c ~= nil then + e.w_set_bkg(args.border_c) + e.window.clear() end - if inner_width % 2 == 0 then alternator = not alternator end + -- draw pattern + for y = start_y, inner_height + (start_y - 1) do + e.w_set_cur(start_x, y) + for _ = 1, inner_width do + if alternator then + if even then + e.w_blit(" ", "00", fill_a .. fill_a) + else + e.w_blit(" ", "0", fill_a) + end + else + if even then + e.w_blit(" ", "00", fill_b .. fill_b) + else + e.w_blit(" ", "0", fill_b) + end + end + + alternator = not alternator + end + + if inner_width % 2 == 0 then alternator = not alternator end + end end + -- initial draw + e.redraw() + return e.complete() end From 625feb3fd14052a3f91f5ef9d4ff0915bb8ff803 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 30 Sep 2023 11:46:47 -0400 Subject: [PATCH 21/35] #344 graphics assertion overhaul --- graphics/core.lua | 8 ++++++ graphics/element.lua | 28 +++++++++++++------ graphics/elements/controls/app.lua | 8 +++--- graphics/elements/controls/checkbox.lua | 4 +-- graphics/elements/controls/hazard_button.lua | 6 ++-- graphics/elements/controls/multi_button.lua | 10 +++---- graphics/elements/controls/push_button.lua | 6 ++-- graphics/elements/controls/radio_2d.lua | 12 ++++---- graphics/elements/controls/radio_button.lua | 14 ++++------ graphics/elements/controls/sidebar.lua | 8 +++--- .../elements/controls/spinbox_numeric.lua | 6 ++-- graphics/elements/controls/switch_button.lua | 8 +++--- graphics/elements/controls/tabbar.lua | 10 +++---- graphics/elements/indicators/alight.lua | 10 +++---- graphics/elements/indicators/coremap.lua | 4 +-- graphics/elements/indicators/data.lua | 8 +++--- graphics/elements/indicators/hbar.lua | 7 ++--- graphics/elements/indicators/icon.lua | 4 +-- graphics/elements/indicators/led.lua | 6 ++-- graphics/elements/indicators/ledpair.lua | 10 +++---- graphics/elements/indicators/ledrgb.lua | 4 +-- graphics/elements/indicators/light.lua | 6 ++-- graphics/elements/indicators/power.lua | 4 +-- graphics/elements/indicators/rad.lua | 8 +++--- graphics/elements/indicators/state.lua | 8 ++---- graphics/elements/indicators/trilight.lua | 10 +++---- graphics/elements/multipane.lua | 2 +- graphics/elements/pipenet.lua | 2 +- graphics/elements/rectangle.lua | 6 ++-- graphics/elements/textbox.lua | 2 +- graphics/elements/tiling.lua | 10 +++---- 31 files changed, 126 insertions(+), 113 deletions(-) diff --git a/graphics/core.lua b/graphics/core.lua index ee7d829..88e0f30 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -111,6 +111,14 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr) } end +-- Assertion Handling + +-- extract the custom element assert message, dropping the path to the element file +function core.extract_assert_msg(msg) + local start = string.find(msg, "@") or 1 + return string.sub(msg, start) +end + -- Interactive Field Manager ---@param e graphics_base diff --git a/graphics/element.lua b/graphics/element.lua index 20bc0c0..7bc8de5 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -2,6 +2,8 @@ -- Generic Graphics Element -- +local util = require("scada-common.util") + local core = require("graphics.core") local events = core.events @@ -65,6 +67,16 @@ local element = {} ---@field key string data key ---@field func function callback +-- more detailed assert message for element verification +---@param condition any assert condition +---@param msg string assert message +---@param callstack_offset? integer shift value to change targets of debug.getinfo() +function element.assert(condition, msg, callstack_offset) + callstack_offset = callstack_offset or 0 + local caller = debug.getinfo(3 + callstack_offset) + assert(condition, util.c(caller.source, ":", caller.currentline, "{", debug.getinfo(2 + callstack_offset).name, "}: ", msg)) +end + -- a base graphics element, should not be created on its own ---@nodiscard ---@param args graphics_args arguments @@ -77,7 +89,7 @@ function element.new(args, child_offset_x, child_offset_y) elem_type = debug.getinfo(2).name, define_completed = false, p_window = nil, ---@type table - position = { x = 1, y = 1 }, ---@type coordinate_2d + position = events.new_coord_2d(1, 1), bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds next_y = 1, -- next child y coordinate next_id = 0, -- next child ID @@ -99,11 +111,9 @@ function element.new(args, child_offset_x, child_offset_y) child_id_map = {} } - local name_brief = "graphics.element{" .. self.elem_type .. "}: " - -- element as string function self.mt.__tostring() - return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self) + return util.c("graphics.element{", self.elem_type, "} @ ", self) end ---@class graphics_element @@ -207,10 +217,10 @@ function element.new(args, child_offset_x, child_offset_y) end -- check frame - assert(f.x >= 1, name_brief .. "frame x not >= 1") - assert(f.y >= 1, name_brief .. "frame y not >= 1") - assert(f.w >= 1, name_brief .. "frame width not >= 1") - assert(f.h >= 1, name_brief .. "frame height not >= 1") + element.assert(f.x >= 1, "frame x not >= 1", 2) + element.assert(f.y >= 1, "frame y not >= 1", 2) + element.assert(f.w >= 1, "frame width not >= 1", 2) + element.assert(f.h >= 1, "frame height not >= 1", 2) -- create window protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true) @@ -409,7 +419,7 @@ function element.new(args, child_offset_x, child_offset_y) end -- check window - assert(self.p_window, name_brief .. "no parent window provided") + element.assert(self.p_window, "no parent window provided", 1) -- prepare the template if args.parent == nil then diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua index 10ef6b5..1433193 100644 --- a/graphics/elements/controls/app.lua +++ b/graphics/elements/controls/app.lua @@ -24,10 +24,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@param args app_button_args ---@return graphics_element element, element_id id local function app_button(args) - assert(type(args.text) == "string", "controls.app: text is a required field") - assert(type(args.title) == "string", "controls.app: title is a required field") - assert(type(args.callback) == "function", "controls.app: callback is a required field") - assert(type(args.app_fg_bg) == "table", "controls.app: app_fg_bg is a required field") + element.assert(type(args.text) == "string", "text is a required field") + element.assert(type(args.title) == "string", "title is a required field") + element.assert(type(args.callback) == "function", "callback is a required field") + element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field") args.height = 4 args.width = 5 diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/checkbox.lua index a31e636..7f0dc58 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -18,8 +18,8 @@ local element = require("graphics.element") ---@param args checkbox_args ---@return graphics_element element, element_id id local function checkbox(args) - assert(type(args.label) == "string", "controls.checkbox: label is a required field") - assert(type(args.box_fg_bg) == "table", "controls.checkbox: box_fg_bg is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field") args.can_focus = true args.height = 1 diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index 9f402cf..9a0e621 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -21,9 +21,9 @@ local element = require("graphics.element") ---@param args hazard_button_args ---@return graphics_element element, element_id id local function hazard_button(args) - assert(type(args.text) == "string", "controls.hazard_button: text is a required field") - assert(type(args.accent) == "number", "controls.hazard_button: accent is a required field") - assert(type(args.callback) == "function", "controls.hazard_button: callback is a required field") + element.assert(type(args.text) == "string", "text is a required field") + element.assert(type(args.accent) == "number", "accent is a required field") + element.assert(type(args.callback) == "function", "callback is a required field") args.height = 3 args.width = string.len(args.text) + 4 diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua index 4de45b0..d686b9d 100644 --- a/graphics/elements/controls/multi_button.lua +++ b/graphics/elements/controls/multi_button.lua @@ -29,11 +29,11 @@ local element = require("graphics.element") ---@param args multi_button_args ---@return graphics_element element, element_id id local function multi_button(args) - assert(type(args.options) == "table", "controls.multi_button: options is a required field") - assert(#args.options > 0, "controls.multi_button: at least one option is required") - assert(type(args.callback) == "function", "controls.multi_button: callback is a required field") - assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "controls.multi_button: default must be nil or a number > 0") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.multi_button: min_width must be nil or a number > 0") + element.assert(type(args.options) == "table", "options is a required field") + element.assert(#args.options > 0, "at least one option is required") + element.assert(type(args.callback) == "function", "callback is a required field") + element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0") + element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") -- single line args.height = 1 diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua index 02bda8a..59e2b9e 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -26,9 +26,9 @@ local KEY_CLICK = core.events.KEY_CLICK ---@param args push_button_args ---@return graphics_element element, element_id id local function push_button(args) - assert(type(args.text) == "string", "controls.push_button: text is a required field") - assert(type(args.callback) == "function", "controls.push_button: callback is a required field") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.push_button: min_width must be nil or a number > 0") + element.assert(type(args.text) == "string", "text is a required field") + element.assert(type(args.callback) == "function", "callback is a required field") + element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") local text_width = string.len(args.text) diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/radio_2d.lua index e4386f1..d87d87e 100644 --- a/graphics/elements/controls/radio_2d.lua +++ b/graphics/elements/controls/radio_2d.lua @@ -27,12 +27,12 @@ local element = require("graphics.element") ---@param args radio_2d_args ---@return graphics_element element, element_id id local function radio_2d_button(args) - assert(type(args.options) == "table" and #args.options > 0, "controls.radio_2d: options should be a table with length >= 1") - assert(util.is_int(args.rows) and util.is_int(args.columns), "controls.radio_2d: rows/columns must be integers") - assert((args.rows * args.columns) >= #args.options, "controls.radio_2d: rows x columns size insufficient for provided number of options") - assert(type(args.radio_colors) == "table", "controls.radio_2d: radio_colors is a required field") - assert(type(args.select_color) == "number" or type(args.color_map) == "table", "controls.radio_2d: select_color or color_map is required") - assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "controls.radio_2d: default must be nil or a number > 0") + element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1") + element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers") + element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options") + element.assert(type(args.radio_colors) == "table", "radio_colors is a required field") + element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required") + element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0") local array = {} local col_widths = {} diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 483f1f8..d54a1df 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -25,14 +25,12 @@ local KEY_CLICK = core.events.KEY_CLICK ---@param args radio_button_args ---@return graphics_element element, element_id id local function radio_button(args) - assert(type(args.options) == "table", "controls.radio_button: options is a required field") - assert(#args.options > 0, "controls.radio_button: at least one option is required") - assert(type(args.radio_colors) == "table", "controls.radio_button: radio_colors is a required field") - assert(type(args.select_color) == "number", "controls.radio_button: select_color is a required field") - assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), - "controls.radio_button: default must be nil or a number > 0") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), - "controls.radio_button: min_width must be nil or a number > 0") + element.assert(type(args.options) == "table", "options is a required field") + element.assert(#args.options > 0, "at least one option is required") + element.assert(type(args.radio_colors) == "table", "radio_colors is a required field") + element.assert(type(args.select_color) == "number", "select_color is a required field") + element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0") + element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") -- determine widths local max_width = 1 diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua index 3ac6bb1..fef8a8a 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/sidebar.lua @@ -27,16 +27,16 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK ---@param args sidebar_args ---@return graphics_element element, element_id id local function sidebar(args) - assert(type(args.tabs) == "table", "controls.sidebar: tabs is a required field") - assert(#args.tabs > 0, "controls.sidebar: at least one tab is required") - assert(type(args.callback) == "function", "controls.sidebar: callback is a required field") + element.assert(type(args.tabs) == "table", "tabs is a required field") + element.assert(#args.tabs > 0, "at least one tab is required") + element.assert(type(args.callback) == "function", "callback is a required field") args.width = 3 -- create new graphics element base object local e = element.new(args) - assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs") + element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs") -- default to 1st tab e.value = 1 diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua index 7615ecf..90edf18 100644 --- a/graphics/elements/controls/spinbox_numeric.lua +++ b/graphics/elements/controls/spinbox_numeric.lua @@ -29,8 +29,8 @@ local function spinbox(args) local wn_prec = args.whole_num_precision local fr_prec = args.fractional_precision - assert(util.is_int(wn_prec), "controls.spinbox_numeric: whole number precision must be an integer") - assert(util.is_int(fr_prec), "controls.spinbox_numeric: fractional precision must be an integer") + element.assert(util.is_int(wn_prec), "whole number precision must be an integer") + element.assert(util.is_int(fr_prec), "fractional precision must be an integer") local fmt, fmt_init ---@type string, string @@ -44,7 +44,7 @@ local function spinbox(args) local dec_point_x = args.whole_num_precision + 1 - assert(type(args.arrow_fg_bg) == "table", "controls.spinbox_numeric: arrow_fg_bg is a required field") + element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field") -- determine widths args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0) diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua index d8da813..f649b10 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -21,10 +21,10 @@ local element = require("graphics.element") ---@param args switch_button_args ---@return graphics_element element, element_id id local function switch_button(args) - assert(type(args.text) == "string", "controls.switch_button: text is a required field") - assert(type(args.callback) == "function", "controls.switch_button: callback is a required field") - assert(type(args.active_fg_bg) == "table", "controls.switch_button: active_fg_bg is a required field") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.switch_button: min_width must be nil or a number > 0") + element.assert(type(args.text) == "string", "text is a required field") + element.assert(type(args.callback) == "function", "callback is a required field") + element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field") + element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") local text_width = string.len(args.text) diff --git a/graphics/elements/controls/tabbar.lua b/graphics/elements/controls/tabbar.lua index ad2c0d7..9b604e1 100644 --- a/graphics/elements/controls/tabbar.lua +++ b/graphics/elements/controls/tabbar.lua @@ -27,10 +27,10 @@ local element = require("graphics.element") ---@param args tabbar_args ---@return graphics_element element, element_id id local function tabbar(args) - assert(type(args.tabs) == "table", "controls.tabbar: tabs is a required field") - assert(#args.tabs > 0, "controls.tabbar: at least one tab is required") - assert(type(args.callback) == "function", "controls.tabbar: callback is a required field") - assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "controls.tabbar: min_width must be nil or a number > 0") + element.assert(type(args.tabs) == "table", "tabs is a required field") + element.assert(#args.tabs > 0, "at least one tab is required") + element.assert(type(args.callback) == "function", "callback is a required field") + element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0") args.height = 1 @@ -48,7 +48,7 @@ local function tabbar(args) -- create new graphics element base object local e = element.new(args) - assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs") + element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs") -- default to 1st tab e.value = 1 diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua index 8ed5337..e05569c 100644 --- a/graphics/elements/indicators/alight.lua +++ b/graphics/elements/indicators/alight.lua @@ -25,13 +25,13 @@ local flasher = require("graphics.flasher") ---@param args alarm_indicator_light ---@return graphics_element element, element_id id local function alarm_indicator_light(args) - assert(type(args.label) == "string", "indicators.alight: label is a required field") - assert(type(args.c1) == "number", "indicators.alight: c1 is a required field") - assert(type(args.c2) == "number", "indicators.alight: c2 is a required field") - assert(type(args.c3) == "number", "indicators.alight: c3 is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.c1) == "number", "c1 is a required field") + element.assert(type(args.c2) == "number", "c2 is a required field") + element.assert(type(args.c3) == "number", "c3 is a required field") if args.flash then - assert(util.is_int(args.period), "indicators.alight: period is a required field if flash is enabled") + element.assert(util.is_int(args.period), "period is a required field if flash is enabled") end -- single line diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/coremap.lua index 12319f6..9084b99 100644 --- a/graphics/elements/indicators/coremap.lua +++ b/graphics/elements/indicators/coremap.lua @@ -18,8 +18,8 @@ local element = require("graphics.element") ---@param args core_map_args ---@return graphics_element element, element_id id local function core_map(args) - assert(util.is_int(args.reactor_l), "indicators.coremap: reactor_l is a required field") - assert(util.is_int(args.reactor_w), "indicators.coremap: reactor_w is a required field") + element.assert(util.is_int(args.reactor_l), "reactor_l is a required field") + element.assert(util.is_int(args.reactor_w), "reactor_w is a required field") -- require max dimensions args.width = 18 diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/data.lua index 6097047..2304807 100644 --- a/graphics/elements/indicators/data.lua +++ b/graphics/elements/indicators/data.lua @@ -24,10 +24,10 @@ local element = require("graphics.element") ---@param args data_indicator_args ---@return graphics_element element, element_id id local function data(args) - assert(type(args.label) == "string", "indicators.data: label is a required field") - assert(type(args.format) == "string", "indicators.data: format is a required field") - assert(args.value ~= nil, "indicators.data: value is a required field") - assert(util.is_int(args.width), "indicators.data: width is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.format) == "string", "format is a required field") + element.assert(args.value ~= nil, "value is a required field") + element.assert(util.is_int(args.width), "width is a required field") args.height = 1 diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/hbar.lua index ef68dc6..eb4607a 100644 --- a/graphics/elements/indicators/hbar.lua +++ b/graphics/elements/indicators/hbar.lua @@ -22,9 +22,6 @@ local element = require("graphics.element") ---@param args hbar_args ---@return graphics_element element, element_id id local function hbar(args) - -- properties/state - local last_num_bars = -1 - -- create new graphics element base object local e = element.new(args) @@ -33,7 +30,9 @@ local function hbar(args) -- bar width is width - 5 characters for " 100%" if showing percent local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w) - assert(bar_width > 0, "indicators.hbar: too small for bar") + element.assert(bar_width > 0, "too small for bar") + + local last_num_bars = -1 -- determine bar colors local bar_bkg = e.fg_bg.blit_bkg diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua index 70d7d2c..5c2fc4e 100644 --- a/graphics/elements/indicators/icon.lua +++ b/graphics/elements/indicators/icon.lua @@ -23,8 +23,8 @@ local element = require("graphics.element") ---@param args icon_indicator_args ---@return graphics_element element, element_id id local function icon(args) - assert(type(args.label) == "string", "indicators.icon: label is a required field") - assert(type(args.states) == "table", "indicators.icon: states is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.states) == "table", "states is a required field") args.height = 1 args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4 diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua index 72e1e93..011ee62 100644 --- a/graphics/elements/indicators/led.lua +++ b/graphics/elements/indicators/led.lua @@ -23,11 +23,11 @@ local flasher = require("graphics.flasher") ---@param args indicator_led_args ---@return graphics_element element, element_id id local function indicator_led(args) - assert(type(args.label) == "string", "indicators.led: label is a required field") - assert(type(args.colors) == "table", "indicators.led: colors is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.colors) == "table", "colors is a required field") if args.flash then - assert(util.is_int(args.period), "indicators.led: period is a required field if flash is enabled") + element.assert(util.is_int(args.period), "period is a required field if flash is enabled") end args.height = 1 diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua index 3ee99fb..1a81854 100644 --- a/graphics/elements/indicators/ledpair.lua +++ b/graphics/elements/indicators/ledpair.lua @@ -25,13 +25,13 @@ local flasher = require("graphics.flasher") ---@param args indicator_led_pair_args ---@return graphics_element element, element_id id local function indicator_led_pair(args) - assert(type(args.label) == "string", "indicators.ledpair: label is a required field") - assert(type(args.off) == "number", "indicators.ledpair: off is a required field") - assert(type(args.c1) == "number", "indicators.ledpair: c1 is a required field") - assert(type(args.c2) == "number", "indicators.ledpair: c2 is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.off) == "number", "off is a required field") + element.assert(type(args.c1) == "number", "c1 is a required field") + element.assert(type(args.c2) == "number", "c2 is a required field") if args.flash then - assert(util.is_int(args.period), "indicators.ledpair: period is a required field if flash is enabled") + element.assert(util.is_int(args.period), "period is a required field if flash is enabled") end args.height = 1 diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua index f2320c6..406e0cc 100644 --- a/graphics/elements/indicators/ledrgb.lua +++ b/graphics/elements/indicators/ledrgb.lua @@ -18,8 +18,8 @@ local element = require("graphics.element") ---@param args indicator_led_rgb_args ---@return graphics_element element, element_id id local function indicator_led_rgb(args) - assert(type(args.label) == "string", "indicators.ledrgb: label is a required field") - assert(type(args.colors) == "table", "indicators.ledrgb: colors is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.colors) == "table", "colors is a required field") args.height = 1 args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2 diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua index 742d6c1..290118c 100644 --- a/graphics/elements/indicators/light.lua +++ b/graphics/elements/indicators/light.lua @@ -23,11 +23,11 @@ local flasher = require("graphics.flasher") ---@param args indicator_light_args ---@return graphics_element element, element_id id local function indicator_light(args) - assert(type(args.label) == "string", "indicators.light: label is a required field") - assert(type(args.colors) == "table", "indicators.light: colors is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.colors) == "table", "colors is a required field") if args.flash then - assert(util.is_int(args.period), "indicators.light: period is a required field if flash is enabled") + element.assert(util.is_int(args.period), "period is a required field if flash is enabled") end args.height = 1 diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua index eaa1f68..96cbfd5 100644 --- a/graphics/elements/indicators/power.lua +++ b/graphics/elements/indicators/power.lua @@ -23,8 +23,8 @@ local element = require("graphics.element") ---@param args power_indicator_args ---@return graphics_element element, element_id id local function power(args) - assert(type(args.value) == "number", "indicators.power: value is a required number field") - assert(util.is_int(args.width), "indicators.power: width is a required field") + element.assert(type(args.value) == "number", "value is a required number field") + element.assert(util.is_int(args.width), "width is a required field") args.height = 1 diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua index 2ae9b6f..5987625 100644 --- a/graphics/elements/indicators/rad.lua +++ b/graphics/elements/indicators/rad.lua @@ -24,10 +24,10 @@ local element = require("graphics.element") ---@param args rad_indicator_args ---@return graphics_element element, element_id id local function rad(args) - assert(type(args.value) ~= "number", "indicators.rad: value is a required number field") - assert(type(args.label) == "string", "indicators.rad: label is a required field") - assert(type(args.format) == "string", "indicators.rad: format is a required field") - assert(util.is_int(args.width), "indicators.rad: width is a required field") + element.assert(type(args.value) ~= "number", "value is a required number field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.format) == "string", "format is a required field") + element.assert(util.is_int(args.width), "width is a required field") args.height = 1 diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/state.lua index 2096c6b..f2dc134 100644 --- a/graphics/elements/indicators/state.lua +++ b/graphics/elements/indicators/state.lua @@ -25,13 +25,11 @@ local element = require("graphics.element") ---@param args state_indicator_args ---@return graphics_element element, element_id id local function state_indicator(args) - assert(type(args.states) == "table", "indicators.state: states is a required field") + element.assert(type(args.states) == "table", "states is a required field") if util.is_int(args.height) then - assert(args.height % 2 == 1, "indicators.state: height should be an odd number") - else - args.height = 1 - end + element.assert(args.height % 2 == 1, "height should be an odd number") + else args.height = 1 end args.width = args.min_width or 1 diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua index f90b21b..f5d441c 100644 --- a/graphics/elements/indicators/trilight.lua +++ b/graphics/elements/indicators/trilight.lua @@ -25,13 +25,13 @@ local flasher = require("graphics.flasher") ---@param args tristate_indicator_light_args ---@return graphics_element element, element_id id local function tristate_indicator_light(args) - assert(type(args.label) == "string", "indicators.trilight: label is a required field") - assert(type(args.c1) == "number", "indicators.trilight: c1 is a required field") - assert(type(args.c2) == "number", "indicators.trilight: c2 is a required field") - assert(type(args.c3) == "number", "indicators.trilight: c3 is a required field") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.c1) == "number", "c1 is a required field") + element.assert(type(args.c2) == "number", "c2 is a required field") + element.assert(type(args.c3) == "number", "c3 is a required field") if args.flash then - assert(util.is_int(args.period), "indicators.trilight: period is a required field if flash is enabled") + element.assert(util.is_int(args.period), "period is a required field if flash is enabled") end args.height = 1 diff --git a/graphics/elements/multipane.lua b/graphics/elements/multipane.lua index 5cfd217..a283ed8 100644 --- a/graphics/elements/multipane.lua +++ b/graphics/elements/multipane.lua @@ -19,7 +19,7 @@ local element = require("graphics.element") ---@param args multipane_args ---@return graphics_element element, element_id id local function multipane(args) - assert(type(args.panes) == "table", "multipane: panes is a required field") + element.assert(type(args.panes) == "table", "panes is a required field") -- create new graphics element base object local e = element.new(args) diff --git a/graphics/elements/pipenet.lua b/graphics/elements/pipenet.lua index ce15c75..625a70d 100644 --- a/graphics/elements/pipenet.lua +++ b/graphics/elements/pipenet.lua @@ -24,7 +24,7 @@ local element = require("graphics.element") ---@param args pipenet_args ---@return graphics_element element, element_id id local function pipenet(args) - assert(type(args.pipes) == "table", "pipenet: pipes is a required field") + element.assert(type(args.pipes) == "table", "pipes is a required field") args.width = 0 args.height = 0 diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua index dfd07c9..252790b 100644 --- a/graphics/elements/rectangle.lua +++ b/graphics/elements/rectangle.lua @@ -22,7 +22,7 @@ local element = require("graphics.element") ---@param args rectangle_args ---@return graphics_element element, element_id id local function rectangle(args) - assert(args.border ~= nil or args.thin ~= true, "rectangle: thin requires border to be provided") + element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided") -- if thin, then width will always need to be 1 if args.thin == true then @@ -65,8 +65,8 @@ local function rectangle(args) local inner_width = e.frame.w - width_x2 -- check dimensions - assert(width_x2 <= e.frame.w, "rectangle: border too thick for width") - assert(width_x2 <= e.frame.h, "rectangle: border too thick for height") + element.assert(width_x2 <= e.frame.w, "border too thick for width") + element.assert(width_x2 <= e.frame.h, "border too thick for height") -- form the basic line strings and top/bottom blit strings local spaces = util.spaces(e.frame.w) diff --git a/graphics/elements/textbox.lua b/graphics/elements/textbox.lua index cfd5585..a6f7207 100644 --- a/graphics/elements/textbox.lua +++ b/graphics/elements/textbox.lua @@ -24,7 +24,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN ---@param args textbox_args ---@return graphics_element element, element_id id local function textbox(args) - assert(type(args.text) == "string", "textbox: text is a required field") + element.assert(type(args.text) == "string", "text is a required field") -- create new graphics element base object local e = element.new(args) diff --git a/graphics/elements/tiling.lua b/graphics/elements/tiling.lua index f40fef5..02e2605 100644 --- a/graphics/elements/tiling.lua +++ b/graphics/elements/tiling.lua @@ -22,7 +22,7 @@ local element = require("graphics.element") ---@param args tiling_args ---@return graphics_element element, element_id id local function tiling(args) - assert(type(args.fill_c) == "table", "tiling: fill_c is a required field") + element.assert(type(args.fill_c) == "table", "fill_c is a required field") -- create new graphics element base object local e = element.new(args) @@ -47,10 +47,10 @@ local function tiling(args) end -- check dimensions - assert(inner_width > 0, "tiling: inner_width <= 0") - assert(inner_height > 0, "tiling: inner_height <= 0") - assert(start_x <= inner_width, "tiling: start_x > inner_width") - assert(start_y <= inner_height, "tiling: start_y > inner_height") + element.assert(inner_width > 0, "inner_width <= 0") + element.assert(inner_height > 0, "inner_height <= 0") + element.assert(start_x <= inner_width, "start_x > inner_width") + element.assert(start_y <= inner_height, "start_y > inner_height") -- draw tiling box function e.redraw() From 560d48084a8629e92cf799303750e66057ba8cbf Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 30 Sep 2023 12:19:04 -0400 Subject: [PATCH 22/35] #344 coordinator renderer assert handling --- coordinator/renderer.lua | 59 +++++++++++++++++----------- coordinator/startup.lua | 5 +-- graphics/core.lua | 2 +- graphics/elements/indicators/rad.lua | 3 +- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index d64ca98..c41f46c 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -15,10 +15,12 @@ local panel_view = require("coordinator.ui.layout.front_panel") local main_view = require("coordinator.ui.layout.main_view") local unit_view = require("coordinator.ui.layout.unit_view") +local core = require("graphics.core") local flasher = require("graphics.flasher") local DisplayBox = require("graphics.elements.displaybox") +---@class coord_renderer local renderer = {} -- render engine @@ -177,35 +179,46 @@ function renderer.close_fp() end -- start the coordinator GUI -function renderer.start_ui() +---@return boolean success, any error_msg +function renderer.try_start_ui() + local status, msg = true, nil + if not engine.ui_ready then -- hide dmesg engine.dmesg_window.setVisible(false) - -- show main view on main monitor - if engine.monitors.primary ~= nil then - engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} - main_view(engine.ui.main_display) + status, msg = pcall(function () + -- show main view on main monitor + if engine.monitors.primary ~= nil then + engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} + main_view(engine.ui.main_display) + end + + -- show flow view on flow monitor + if engine.monitors.flow ~= nil then + engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root} + flow_view(engine.ui.flow_display) + end + + -- show unit views on unit displays + for idx, display in pairs(engine.monitors.unit_displays) do + engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root} + unit_view(engine.ui.unit_displays[idx], idx) + end + end) + + if status then + -- start flasher callback task and report ready + flasher.run() + engine.ui_ready = true + else + -- report fail and close ui + msg = core.extract_assert_msg(msg) + renderer.close_ui() end - - -- show flow view on flow monitor - if engine.monitors.flow ~= nil then - engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root} - flow_view(engine.ui.flow_display) - end - - -- show unit views on unit displays - for idx, display in pairs(engine.monitors.unit_displays) do - engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root} - unit_view(engine.ui.unit_displays[idx], idx) - end - - -- start flasher callback task - flasher.run() - - -- report ui as ready - engine.ui_ready = true end + + return status, msg end -- close out the UI diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 145f77b..cf902fc 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.0.14" +local COORDINATOR_VERSION = "v1.0.15" local println = util.println local println_ts = util.println_ts @@ -198,9 +198,8 @@ local function main() local draw_start = util.time_ms() - local ui_ok, ui_message = pcall(renderer.start_ui) + local ui_ok, ui_message = renderer.try_start_ui() if not ui_ok then - renderer.close_ui() log_graphics(util.c("main UI error: ", ui_message)) log.fatal(util.c("main GUI render failed with error ", ui_message)) else diff --git a/graphics/core.lua b/graphics/core.lua index 88e0f30..2f17d98 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -115,7 +115,7 @@ end -- extract the custom element assert message, dropping the path to the element file function core.extract_assert_msg(msg) - local start = string.find(msg, "@") or 1 + local start = (string.find(msg, "@") + 1) or 1 return string.sub(msg, start) end diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua index 5987625..d13655e 100644 --- a/graphics/elements/indicators/rad.lua +++ b/graphics/elements/indicators/rad.lua @@ -10,7 +10,7 @@ local element = require("graphics.element") ---@field format string data format (lua string format) ---@field commas? boolean whether to use commas if a number is given (default to false) ---@field lu_colors? cpair label foreground color (a), unit foreground color (b) ----@field value number default value +---@field value? radiation_reading default value ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -24,7 +24,6 @@ local element = require("graphics.element") ---@param args rad_indicator_args ---@return graphics_element element, element_id id local function rad(args) - element.assert(type(args.value) ~= "number", "value is a required number field") element.assert(type(args.label) == "string", "label is a required field") element.assert(type(args.format) == "string", "format is a required field") element.assert(util.is_int(args.width), "width is a required field") From d38a2dea7cd9a5cf7816296fd5f49e4df5e24c50 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 30 Sep 2023 13:31:41 -0400 Subject: [PATCH 23/35] #344 renderer integration with new assertion handling --- coordinator/renderer.lua | 31 +++++++++++++++++++++---------- coordinator/startup.lua | 5 ++--- pocket/renderer.lua | 27 +++++++++++++++++++++------ pocket/startup.lua | 5 ++--- reactor-plc/renderer.lua | 27 +++++++++++++++++++++------ reactor-plc/startup.lua | 5 ++--- rtu/renderer.lua | 29 ++++++++++++++++++++++------- rtu/startup.lua | 5 ++--- supervisor/renderer.lua | 27 +++++++++++++++++++++------ supervisor/startup.lua | 5 ++--- 10 files changed, 116 insertions(+), 50 deletions(-) diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index c41f46c..5aadbb2 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -133,19 +133,30 @@ function renderer.init_dmesg() log.direct_dmesg(engine.dmesg_window) end --- start the coordinator front panel -function renderer.start_fp() +-- try to start the front panel +---@return boolean success, any error_msg +function renderer.try_start_fp() + local status, msg = true, nil + if not engine.fp_ready then -- show front panel view on terminal - engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root} - panel_view(engine.ui.front_panel, #engine.monitors.unit_displays) + status, msg = pcall(function () + engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root} + panel_view(engine.ui.front_panel, #engine.monitors.unit_displays) + end) - -- start flasher callback task - flasher.run() - - -- report front panel as ready - engine.fp_ready = true + if status then + -- start flasher callback task and report ready + flasher.run() + engine.fp_ready = true + else + -- report fail and close front panel + msg = core.extract_assert_msg(msg) + renderer.close_fp() + end end + + return status, msg end -- close out the front panel @@ -178,7 +189,7 @@ function renderer.close_fp() end end --- start the coordinator GUI +-- try to start the main GUI ---@return boolean success, any error_msg function renderer.try_start_ui() local status, msg = true, nil diff --git a/coordinator/startup.lua b/coordinator/startup.lua index cf902fc..0ce23ba 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.0.15" +local COORDINATOR_VERSION = "v1.0.16" local println = util.println local println_ts = util.println_ts @@ -182,9 +182,8 @@ local function main() log_graphics("starting front panel UI...") - local fp_ok, fp_message = pcall(renderer.start_fp) + local fp_ok, fp_message = renderer.try_start_fp() if not fp_ok then - renderer.close_fp() log_graphics(util.c("front panel UI error: ", fp_message)) println_ts("front panel UI creation failed") log.fatal(util.c("front panel GUI render failed with error ", fp_message)) diff --git a/pocket/renderer.lua b/pocket/renderer.lua index fa25bcd..892fc92 100644 --- a/pocket/renderer.lua +++ b/pocket/renderer.lua @@ -5,18 +5,23 @@ local main_view = require("pocket.ui.main") local style = require("pocket.ui.style") +local core = require("graphics.core") local flasher = require("graphics.flasher") local DisplayBox = require("graphics.elements.displaybox") +---@class pocket_renderer local renderer = {} local ui = { display = nil } --- start the pocket GUI -function renderer.start_ui() +-- try to start the pocket GUI +---@return boolean success, any error_msg +function renderer.try_start_ui() + local status, msg = true, nil + if ui.display == nil then -- reset screen term.setTextColor(colors.white) @@ -30,12 +35,22 @@ function renderer.start_ui() end -- init front panel view - ui.display = DisplayBox{window=term.current(),fg_bg=style.root} - main_view(ui.display) + status, msg = pcall(function () + ui.display = DisplayBox{window=term.current(),fg_bg=style.root} + main_view(ui.display) + end) - -- start flasher callback task - flasher.run() + if status then + -- start flasher callback task + flasher.run() + else + -- report fail and close ui + msg = core.extract_assert_msg(msg) + renderer.close_ui() + end end + + return status, msg end -- close out the UI diff --git a/pocket/startup.lua b/pocket/startup.lua index 0592d6c..68fb2c1 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "v0.6.2-alpha" +local POCKET_VERSION = "v0.6.3-alpha" local println = util.println local println_ts = util.println_ts @@ -111,9 +111,8 @@ local function main() -- start the UI ---------------------------------------- - local ui_ok, message = pcall(renderer.start_ui) + local ui_ok, message = renderer.try_start_ui() if not ui_ok then - renderer.close_ui() println(util.c("UI error: ", message)) log.error(util.c("startup> GUI render failed with error ", message)) else diff --git a/reactor-plc/renderer.lua b/reactor-plc/renderer.lua index 038918b..2613600 100644 --- a/reactor-plc/renderer.lua +++ b/reactor-plc/renderer.lua @@ -5,18 +5,23 @@ local panel_view = require("reactor-plc.panel.front_panel") local style = require("reactor-plc.panel.style") +local core = require("graphics.core") local flasher = require("graphics.flasher") local DisplayBox = require("graphics.elements.displaybox") +---@class reactor_plc_renderer local renderer = {} local ui = { display = nil } --- start the UI -function renderer.start_ui() +-- try to start the UI +---@return boolean success, any error_msg +function renderer.try_start_ui() + local status, msg = true, nil + if ui.display == nil then -- reset terminal term.setTextColor(colors.white) @@ -30,12 +35,22 @@ function renderer.start_ui() end -- init front panel view - ui.display = DisplayBox{window=term.current(),fg_bg=style.root} - panel_view(ui.display) + status, msg = pcall(function () + ui.display = DisplayBox{window=term.current(),fg_bg=style.root} + panel_view(ui.display) + end) - -- start flasher callback task - flasher.run() + if status then + -- start flasher callback task + flasher.run() + else + -- report fail and close ui + msg = core.extract_assert_msg(msg) + renderer.close_ui() + end end + + return status, msg end -- close out the UI diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e008280..97ac227 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -19,7 +19,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.5.9" +local R_PLC_VERSION = "v1.5.10" local println = util.println local println_ts = util.println_ts @@ -184,10 +184,9 @@ local function main() -- front panel time! if not renderer.ui_ready() then local message - plc_state.fp_ok, message = pcall(renderer.start_ui) + plc_state.fp_ok, message = renderer.try_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("front panel GUI render failed with error ", message)) diff --git a/rtu/renderer.lua b/rtu/renderer.lua index 17949ce..a212c39 100644 --- a/rtu/renderer.lua +++ b/rtu/renderer.lua @@ -5,19 +5,24 @@ local panel_view = require("rtu.panel.front_panel") local style = require("rtu.panel.style") +local core = require("graphics.core") local flasher = require("graphics.flasher") local DisplayBox = require("graphics.elements.displaybox") +---@class rtu_renderer local renderer = {} local ui = { display = nil } --- start the UI +-- try to start the UI ---@param units table RTU units -function renderer.start_ui(units) +---@return boolean success, any error_msg +function renderer.try_start_ui(units) + local status, msg = true, nil + if ui.display == nil then -- reset terminal term.setTextColor(colors.white) @@ -30,13 +35,23 @@ function renderer.start_ui(units) 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) + status, msg = pcall(function () + ui.display = DisplayBox{window=term.current(),fg_bg=style.root} + panel_view(ui.display, units) + end) + + if status then + -- start flasher callback task + flasher.run() + else + -- report fail and close ui + msg = core.extract_assert_msg(msg) + renderer.close_ui() + end end + + return status, msg end -- close out the UI diff --git a/rtu/startup.lua b/rtu/startup.lua index 1ed19e3..f40df57 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.6.5" +local RTU_VERSION = "v1.6.6" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -480,10 +480,9 @@ local function main() if configure() then -- start UI local message - rtu_state.fp_ok, message = pcall(renderer.start_ui, units) + rtu_state.fp_ok, message = renderer.try_start_ui(units) if not rtu_state.fp_ok then - renderer.close_ui() println_ts(util.c("UI error: ", message)) println("startup> running without front panel") log.error(util.c("front panel GUI render failed with error ", message)) diff --git a/supervisor/renderer.lua b/supervisor/renderer.lua index 1bc70a4..73333d0 100644 --- a/supervisor/renderer.lua +++ b/supervisor/renderer.lua @@ -6,18 +6,23 @@ local panel_view = require("supervisor.panel.front_panel") local pgi = require("supervisor.panel.pgi") local style = require("supervisor.panel.style") +local core = require("graphics.core") local flasher = require("graphics.flasher") local DisplayBox = require("graphics.elements.displaybox") +---@class supervisor_renderer local renderer = {} local ui = { display = nil } --- start the UI -function renderer.start_ui() +-- try to start the UI +---@return boolean success, any error_msg +function renderer.try_start_ui() + local status, msg = true, nil + if ui.display == nil then -- reset terminal term.setTextColor(colors.white) @@ -31,12 +36,22 @@ function renderer.start_ui() end -- init front panel view - ui.display = DisplayBox{window=term.current(),fg_bg=style.root} - panel_view(ui.display) + status, msg = pcall(function () + ui.display = DisplayBox{window=term.current(),fg_bg=style.root} + panel_view(ui.display) + end) - -- start flasher callback task - flasher.run() + if status then + -- start flasher callback task + flasher.run() + else + -- report fail and close ui + msg = core.extract_assert_msg(msg) + renderer.close_ui() + end end + + return status, msg end -- close out the UI diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 4cc4195..11f0948 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.0.5" +local SUPERVISOR_VERSION = "v1.0.6" local println = util.println local println_ts = util.println_ts @@ -117,10 +117,9 @@ local function main() databus.tx_hw_modem(true) -- start UI - local fp_ok, message = pcall(renderer.start_ui) + local fp_ok, message = renderer.try_start_ui() if not fp_ok then - renderer.close_ui() println_ts(util.c("UI error: ", message)) log.error(util.c("front panel GUI render failed with error ", message)) else From 3a5d69d96fcdaf040e277791675819cac0718521 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 00:18:57 -0400 Subject: [PATCH 24/35] improvements to number field --- graphics/elements/form/number_field.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index f444073..b519c8a 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -98,11 +98,17 @@ local function number_field(args) -- set minimum input value ---@param min integer minimum allowed value - function e.set_min(min) args.min = min end + function e.set_min(min) + args.min = min + e.on_unfocused() + end -- set maximum input value ---@param max integer maximum allowed value - function e.set_max(max) args.max = max end + function e.set_max(max) + args.max = max + e.on_unfocused() + end -- replace text with pasted text if its a number ---@param text string string pasted @@ -123,8 +129,10 @@ local function number_field(args) if type(val) == "number" then if type(args.max) == "number" and val > max then e.value = "" .. max + ifield.nav_start() elseif type(args.min) == "number" and val < min then e.value = "" .. min + ifield.nav_start() end else e.value = "" From 4d4dd4ed39a2065156edf622aa15607f0bd99330 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 00:19:16 -0400 Subject: [PATCH 25/35] fix to redraw and improvements to hide() --- graphics/element.lua | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/graphics/element.lua b/graphics/element.lua index 7bc8de5..2ace4e1 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -775,11 +775,14 @@ function element.new(args, child_offset_x, child_offset_y) -- hide the element and disables animations
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown
+ ---@see graphics_element.redraw ---@see graphics_element.content_redraw - function public.hide() + ---@param clear? boolean true to visibly hide this element (redraws the parent) + function public.hide(clear) public.freeze_all() -- stop animations for efficiency/performance public.unfocus_all() protected.window.setVisible(false) + if clear and args.parent then args.parent.redraw() end end -- start/resume animation(s) @@ -803,8 +806,14 @@ function element.new(args, child_offset_x, child_offset_y) for _, child in pairs(protected.children) do child.get().freeze_all() end end - -- re-draw the element - function public.redraw() protected.window.redraw() end + -- re-draw this element and all its children + function public.redraw() + protected.window.setBackgroundColor(protected.fg_bg.bkg) + protected.window.setTextColor(protected.fg_bg.fgd) + protected.window.clear() + protected.redraw() + for _, child in pairs(protected.children) do child.get().redraw() end + end -- if a content window is set, clears it then re-draws all children function public.content_redraw() From c0a602385d6343012f78173ad77fdbc6942d9344 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 00:20:19 -0400 Subject: [PATCH 26/35] recycle log at <512B free --- scada-common/log.lua | 2 +- scada-common/util.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index eadc763..8a42e45 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -54,7 +54,7 @@ local function _log(msg) end end - if out_of_space or (free_space(logger.path) < 100) then + if out_of_space or (free_space(logger.path) < 512) then -- delete the old log file before opening a new one logger.file.close() fs.delete(logger.path) diff --git a/scada-common/util.lua b/scada-common/util.lua index 144f88d..a1a1b4a 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -8,7 +8,7 @@ local cc_strings = require("cc.strings") local util = {} -- scada-common version -util.version = "1.1.1" +util.version = "1.1.2" -- ENVIRONMENT CONSTANTS -- From 21d5cb3858342449cc2a5547b2699f8f0f5f0bba Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 00:21:46 -0400 Subject: [PATCH 27/35] #307 reactor PLC configurator --- reactor-plc/configure.lua | 548 +++++++++++++++++++++++++++----------- 1 file changed, 391 insertions(+), 157 deletions(-) diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 59a7e4f..e1da81c 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -2,30 +2,26 @@ -- Configuration GUI -- -local log = require("scada-common.log") -local tcd = require("scada-common.tcd") -local util = require("scada-common.util") +local log = require("scada-common.log") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") -log.init("/log.txt", log.MODE.APPEND, true) +local core = require("graphics.core") -local core = require("graphics.core") +local DisplayBox = require("graphics.elements.displaybox") +local Div = require("graphics.elements.div") +local ListBox = require("graphics.elements.listbox") +local MultiPane = require("graphics.elements.multipane") +local TextBox = require("graphics.elements.textbox") -local DisplayBox = require("graphics.elements.displaybox") -local Div = require("graphics.elements.div") -local MultiPane = require("graphics.elements.multipane") -local Rectangle = require("graphics.elements.rectangle") -local TextBox = require("graphics.elements.textbox") - -local RadioButton = require("graphics.elements.controls.radio_button") -local PushButton = require("graphics.elements.controls.push_button") -local CheckBox = require("graphics.elements.controls.checkbox") +local CheckBox = require("graphics.elements.controls.checkbox") +local PushButton = require("graphics.elements.controls.push_button") +local RadioButton = require("graphics.elements.controls.radio_button") +local Radio2D = require("graphics.elements.controls.radio_2d") local NumberField = require("graphics.elements.form.number_field") -local TextField = require("graphics.elements.form.text_field") +local TextField = require("graphics.elements.form.text_field") -local ListBox = require("graphics.elements.listbox") - -local print = util.print local println = util.println local cpair = core.cpair @@ -55,51 +51,112 @@ style.colors = { { c = colors.purple, hex = 0xb156ee }, { c = colors.pink, hex = 0xf26ba2 }, { c = colors.magenta, hex = 0xf9488a }, - -- { c = colors.white, hex = 0xf0f0f0 }, { c = colors.lightGray, hex = 0xcacaca }, - { c = colors.gray, hex = 0x575757 }, - -- { c = colors.black, hex = 0x191919 }, - -- { c = colors.brown, hex = 0x7f664c } + { c = colors.gray, hex = 0x575757 } } local tool_ctl = { - need_config = false, + ask_config = false, + has_config = false, + viewing_config = false, + importing_legacy = false, - set_networked = nil, ---@type function - next_from_plc = nil, ---@type function - back_from_log = nil, ---@type function - gen_summary = nil ---@type function + view_cfg = nil, ---@type graphics_element + settings_apply = nil, ---@type graphics_element + + set_networked = nil, ---@type function + bundled_emcool = nil, ---@type function + gen_summary = nil, ---@type function + show_current_cfg = nil, ---@type function + load_legacy = nil ---@type function } +---@class _plc_cfg local tmp_cfg = { Networked = false, UnitID = 0, - SVR_Channel = 0, - PLC_Channel = 0, - ConnTimeout = 0, - TrustedRange = 0, - AuthKey = "", + EmerCoolEnable = false, + EmerCoolSide = nil, + EmerCoolColor = nil, + SVR_Channel = nil, + PLC_Channel = nil, + ConnTimeout = nil, + TrustedRange = nil, + AuthKey = nil, LogMode = 0, LogPath = "", LogDebug = false, } +---@class _plc_cfg +local ini_cfg = {} + local fields = { - -- printed name, tmp_cfg name, requires_network - { "Networked", "Networked", false }, - { "Unit ID", "UnitID", false }, - { "SVR Channel", "SVR_Channel", true }, - { "PLC Channel", "PLC_Channel", true }, - { "Connection Timeout", "ConnTimeout", true }, - { "Trusted Range", "TrustedRange", true }, - { "Facility Auth Key", "AuthKey", true }, - { "Log Mode", "LogMode", false }, - { "Log Path", "LogPath", false }, - { "Log Debug Messages", "LogDebug", false } + { "Networked", "Networked" }, + { "UnitID", "Unit ID" }, + { "EmerCoolEnable", "Emergency Coolant" }, + { "EmerCoolSide", "Emergency Coolant Side" }, + { "EmerCoolColor", "Emergency Coolant Color" }, + { "SVR_Channel", "SVR Channel" }, + { "PLC_Channel", "PLC Channel" }, + { "ConnTimeout", "Connection Timeout" }, + { "TrustedRange", "Trusted Range" }, + { "AuthKey", "Facility Auth Key" }, + { "LogMode", "Log Mode" }, + { "LogPath", "Log Path" }, + { "LogDebug","Log Debug Messages" } } -local function _config_view(display) - -- window header message +local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" } +local side_options_map = { "top", "bottom", "left", "right", "front", "back" } +local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" } +local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown } + +-- convert text representation to index +---@param side string +local function side_to_idx(side) + for k, v in ipairs(side_options_map) do + if v == side then return k end + end +end + +-- convert text representation to index +---@param color string +local function color_to_idx(color) + for k, v in ipairs(color_options_map) do + if v == color then return k end + end +end + +-- load data from the settings file +---@param target _plc_cfg +local function load_settings(target) + target.Networked = settings.get("Networked", false) + target.UnitID = settings.get("UnitID", 1) + target.EmerCoolEnable = settings.get("EmerCoolEnable", false) + target.EmerCoolSide = settings.get("EmerCoolSide", nil) + target.EmerCoolColor = settings.get("EmerCoolColor", nil) + target.SVR_Channel = settings.get("SVR_Channel", 16240) + target.PLC_Channel = settings.get("PLC_Channel", 16241) + target.ConnTimeout = settings.get("ConnTimeout", 5) + target.TrustedRange = settings.get("TrustedRange", 0) + target.AuthKey = settings.get("AuthKey", "") + target.LogMode = settings.get("LogMode", 0) + target.LogPath = settings.get("LogPath", "/log.txt") + target.LogDebug = settings.get("LogDebug", false) +end + +-- create the config view +---@param display graphics_element +local function config_view(display) + local nav_fg_bg = cpair(colors.black,colors.white) + local btn_act_fg_bg = cpair(colors.white,colors.gray) + + local function exit() +---@diagnostic disable-next-line: undefined-field + os.queueEvent("terminate") + end + TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,height=1,fg_bg=style.header} local root_pane_div = Div{parent=display,x=1,y=2} @@ -118,54 +175,102 @@ local function _config_view(display) TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."} - if tool_ctl.need_config then + if tool_ctl.ask_config then y_offset = 3 - TextBox{parent=main_page,x=2,y=5,height=2,text_align=CENTER,text="Notice: This Reactor PLC is not configured. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)} + TextBox{parent=main_page,x=2,y=5,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)} end - PushButton{parent=main_page,x=2,y=y_offset+5,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} - PushButton{parent=main_page,x=2,y=y_offset+7,min_width=20,text="View Configuration",callback=function()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=cpair(colors.white,colors.gray)} + local function view_config() + tool_ctl.viewing_config = true + tool_ctl.gen_summary(ini_cfg) + tool_ctl.settings_apply.hide(true) + main_pane.set_value(5) + end if fs.exists("/reactor-plc/config.lua") then - PushButton{parent=main_page,x=2,y=y_offset+9,min_width=28,text="Import Legacy 'config.lua'",callback=function()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=cpair(colors.white,colors.gray)} + y_offset = y_offset + 5 + PushButton{parent=main_page,x=2,y=y_offset,min_width=28,text="Import Legacy 'config.lua'",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg} end ----@diagnostic disable-next-line: undefined-field - PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=function()os.queueEvent("terminate")end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=main_page,x=2,y=y_offset+2,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_offset+4,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} - local nav_fg_bg = cpair(colors.black,colors.white) - local nav_a_fg_bg = cpair(colors.white,colors.gray) + if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end + + PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} -- PLC CONFIG local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49} local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49} + local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49} + local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49} - local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2}} + local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}} TextBox{parent=plc_cfg,x=1,y=2,height=1,text_align=CENTER,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)} TextBox{parent=plc_c_1,x=1,y=1,height=1,text_align=CENTER,text="Would you like to set this PLC as networked?"} TextBox{parent=plc_c_1,x=1,y=3,height=4,text_align=CENTER,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=cpair(colors.gray,colors.lightGray)} - CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.set_networked(v)end} + local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.set_networked(v)end} - PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=plc_c_2,x=1,y=1,height=1,text_align=CENTER,text="Please enter the reactor unit ID for this PLC."} TextBox{parent=plc_c_2,x=1,y=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,text="Unit #"} - local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=1,min=1,fg_bg=cpair(colors.black,colors.white)} + local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=cpair(colors.black,colors.white)} + + local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local function submit_id() - tmp_cfg.UnitID = u_id.get_value() - tool_ctl.next_from_plc() + local unit_id = tonumber(u_id.get_value()) + if unit_id ~= nil then + u_id_err.hide(true) + tmp_cfg.UnitID = unit_id + plc_pane.set_value(3) + else u_id_err.show() end end - PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=plc_c_3,x=1,y=1,height=4,text_align=CENTER,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "} + TextBox{parent=plc_c_3,x=1,y=6,height=5,text_align=CENTER,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=cpair(colors.gray,colors.lightGray)} + + local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",box_fg_bg=cpair(colors.orange,colors.black),value=ini_cfg.EmerCoolEnable} + + local function next_from_plc() + if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end + end + + local function submit_en_emcool() + tmp_cfg.EmerCoolEnable = en_em_cool.get_value() + if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end + end + + PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=plc_c_4,x=1,y=1,height=1,text_align=CENTER,text="Emergency Coolant Redstone Output Side"} + local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,value=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange} + + TextBox{parent=plc_c_4,x=1,y=5,height=1,text_align=CENTER,text="Bundled Redstone Configuration"} + local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end} + local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,value=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=cpair(colors.gray,colors.lightGray)} + color.disable() + + local function submit_emcool() + tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] + tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil) + next_from_plc() + end + + PushButton{parent=plc_c_4,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=plc_c_4,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} -- NET CONFIG @@ -181,44 +286,68 @@ local function _config_view(display) TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"} - local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=16240,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} + local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,text="PLC Channel"} - local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=16241,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} + local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)} + local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + local function submit_channels() - tmp_cfg.SVR_Channel = svr_chan.get_value() - tmp_cfg.PLC_Channel = plc_chan.get_value() - net_pane.set_value(2) + local svr_c = tonumber(svr_chan.get_value()) + local plc_c = tonumber(plc_chan.get_value()) + if svr_c ~= nil and plc_c ~= nil then + tmp_cfg.SVR_Channel = svr_c + tmp_cfg.PLC_Channel = plc_c + net_pane.set_value(2) + chan_err.hide(true) + elseif svr_c == nil then + chan_err.set_value("Please set the supervisor channel.") + chan_err.show() + else + chan_err.set_value("Please set the PLC channel.") + chan_err.show() + end end - PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,text="Connection Timeout"} - local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=5,min=2,max=25,fg_bg=cpair(colors.black,colors.white)} + local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=net_c_2,x=9,y=2,height=2,text_align=CENTER,text="seconds (default 5)",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_2,x=1,y=3,height=4,text_align=CENTER,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_2,x=1,y=8,height=1,text_align=CENTER,text="Trusted Range"} - local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=0,min=0,max_digits=20,allow_decimal=true,fg_bg=cpair(colors.black,colors.white)} + local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=20,allow_decimal=true,fg_bg=cpair(colors.black,colors.white)} TextBox{parent=net_c_2,x=1,y=10,height=4,text_align=CENTER,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=cpair(colors.gray,colors.lightGray)} + local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + local function submit_ct_tr() - tmp_cfg.ConnTimeout = timeout.get_value() - tmp_cfg.TrustedRange = range.get_value() - net_pane.set_value(3) + if tonumber(timeout.get_value()) ~= nil and tonumber(range.get_value()) ~= nil then + tmp_cfg.ConnTimeout = timeout.get_value() + tmp_cfg.TrustedRange = range.get_value() + net_pane.set_value(3) + p2_err.hide(true) + elseif tonumber(timeout.get_value()) == nil then + p2_err.set_value("Please set the connection timeout.") + p2_err.show() + else + p2_err.set_value("Please set the trusted range.") + p2_err.show() + end end - PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=net_c_3,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} TextBox{parent=net_c_3,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"} - local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} + local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=function(v)censor(util.trinary(v,"*",nil))end} local function submit_auth() @@ -226,8 +355,8 @@ local function _config_view(display) main_pane.set_value(4) end - PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=net_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} -- LOG CONFIG @@ -238,45 +367,100 @@ local function _config_view(display) TextBox{parent=log_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."} TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,text="Log File Mode"} - local mode = RadioButton{parent=log_c_1,x=1,y=4,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + local mode = RadioButton{parent=log_c_1,x=1,y=4,value=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"} - local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value="/log.txt",max_len=128,fg_bg=cpair(colors.black,colors.white)} + local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=cpair(colors.black,colors.white)} - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} + local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,value=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)} + local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + local function submit_log() - tmp_cfg.LogMode = mode.get_value() - tmp_cfg.LogPath = path.get_value() - tmp_cfg.LogDebug = en_dbg.get_value() - tool_ctl.gen_summary() - main_pane.set_value(5) + if path.get_value() ~= "" then + path_err.hide(true) + tmp_cfg.LogMode = mode.get_value() - 1 + tmp_cfg.LogPath = path.get_value() + tmp_cfg.LogDebug = en_dbg.get_value() + tool_ctl.gen_summary(tmp_cfg) + tool_ctl.viewing_config = false + tool_ctl.importing_legacy = false + tool_ctl.settings_apply.show() + main_pane.set_value(5) + else path_err.show() end end - PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()tool_ctl.back_from_log()end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} + local function back_from_log() + if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end + end + + PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} -- SUMMARY OF CHANGES local sum_c_1 = Div{parent=summary,x=2,y=4,width=49} local sum_c_2 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_3 = Div{parent=summary,x=2,y=4,width=49} + local sum_c_4 = Div{parent=summary,x=2,y=4,width=49} - local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2}} + local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}} TextBox{parent=summary,x=1,y=2,height=1,text_align=CENTER,text=" Summary",fg_bg=cpair(colors.black,colors.green)} local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=cpair(colors.black,colors.white),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} - local function save_and_continue() - -- settings.load(".plc.settings") - -- settings.set("UnitID", tmp_cfg.UnitID) - -- settings.save(".plc.settings") - sum_pane.set_value(2) + local function back_from_settings() + if tool_ctl.viewing_config or tool_ctl.importing_legacy then + main_pane.set_value(1) + tool_ctl.viewing_config = false + tool_ctl.importing_legacy = false + tool_ctl.settings_apply.show() + else + main_pane.set_value(4) + end end - PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} - PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=nav_a_fg_bg} + ---@param element graphics_element + ---@param data any + local function try_set(element, data) + if data ~= nil then element.set_value(data) end + end + + local function save_and_continue() + for k, v in pairs(tmp_cfg) do settings.set(k, v) end + + if settings.save("rplc.settings") then + load_settings(tmp_cfg) + + try_set(networked, tmp_cfg.Networked) + try_set(u_id, tmp_cfg.UnitID) + try_set(en_em_cool, tmp_cfg.EmerCoolEnable) + try_set(side, tmp_cfg.EmerCoolSide) + try_set(color, tmp_cfg.EmerCoolColor) + try_set(svr_chan, tmp_cfg.SVR_Channel) + try_set(plc_chan, tmp_cfg.PLC_Channel) + try_set(timeout, tmp_cfg.ConnTimeout) + try_set(range, tmp_cfg.TrustedRange) + try_set(key, tmp_cfg.AuthKey) + try_set(mode, tmp_cfg.LogMode) + try_set(path, tmp_cfg.LogPath) + try_set(en_dbg, tmp_cfg.LogDebug) + + if tool_ctl.importing_legacy then + tool_ctl.importing_legacy = false + sum_pane.set_value(3) + else + sum_pane.set_value(2) + end + else + sum_pane.set_value(4) + end + end + + PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"} @@ -287,26 +471,67 @@ local function _config_view(display) sum_pane.set_value(1) end - PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=nav_a_fg_bg} ----@diagnostic disable-next-line: undefined-field - PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=function()os.queueEvent("terminate")end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} - -- overwrite functions now that we have the elements + TextBox{parent=sum_c_3,x=1,y=1,height=2,text_align=CENTER,text="The old config.lua file will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/reactor-plc/config.lua") + exit() + end + + PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)} + + TextBox{parent=sum_c_4,x=1,y=1,height=5,text_align=CENTER,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} + + PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + -- set tool functions now that we have the elements function tool_ctl.set_networked(enable) tmp_cfg.Networked = enable if enable then u_id.set_max(4) else u_id.set_max(999) end end - function tool_ctl.next_from_plc() - if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end + function tool_ctl.bundled_emcool(en) if en then color.enable() else color.disable() end end + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("reactor-plc.config") + + tmp_cfg.Networked = config.NETWORKED + tmp_cfg.UnitID = config.REACTOR_ID + tmp_cfg.EmerCoolEnable = type(config.EMERGENCY_COOL) == "table" + + if tmp_cfg.EmerCoolEnable then + tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side + tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color + else + tmp_cfg.EmerCoolSide = nil + tmp_cfg.EmerCoolColor = nil + end + + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.PLC_Channel = config.PLC_CHANNEL + tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT + tmp_cfg.TrustedRange = config.TRUSTED_RANGE + tmp_cfg.AuthKey = config.AUTH_KEY or "" + tmp_cfg.LogMode = config.LOG_MODE + tmp_cfg.LogPath = config.LOG_PATH + tmp_cfg.LogDebug = config.LOG_DEBUG + + tool_ctl.gen_summary(tmp_cfg) + sum_pane.set_value(1) + main_pane.set_value(5) + tool_ctl.importing_legacy = true end - function tool_ctl.back_from_log() - if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end - end - - function tool_ctl.gen_summary() + -- generate the summary list + ---@param cfg _plc_cfg + function tool_ctl.gen_summary(cfg) setting_list.remove_all() local alternate = false @@ -315,23 +540,24 @@ local function _config_view(display) for i = 1, #fields do local f = fields[i] local height = 1 - local label_w = string.len(f[1]) + local label_w = string.len(f[2]) local val_max_w = (inner_width - label_w) + 1 - local val = util.strval(tmp_cfg[f[2]]) + local val = util.strval(cfg[f[1]]) - if f[3] and not tmp_cfg.Networked then val = "n/a" end - if f[2] == "AuthKey" and hide_key.get_value() then val = string.rep("*", string.len(val)) end + if f[1] == "AuthKey" and hide_key.get_value() then val = string.rep("*", string.len(val)) end + if f[1] == "LogMode" then val = util.trinary(cfg[f[1]] == log.MODE.APPEND, "append", "replace") end + if val == "nil" then val = "n/a" end local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white)) alternate = not alternate - if (f[2] == "LogPath" or f[2] == "AuthKey") and string.len(val) > val_max_w then + if string.len(val) > val_max_w then local lines = util.strwrap(val, inner_width) height = #lines + 1 end local line = Div{parent=setting_list,height=height,fg_bg=c} - TextBox{parent=line,text=f[1],width=string.len(f[1]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} if height > 1 then TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT} @@ -342,61 +568,69 @@ local function _config_view(display) end end -function configurator.configure(need_config) - log.debug("configurator started") - - tool_ctl.need_config = true - - -- reset terminal +-- reset terminal screen +local function reset_term() term.setTextColor(colors.white) term.setBackgroundColor(colors.black) term.clear() term.setCursorPos(1, 1) +end + +-- run the reactor PLC configurator +---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration +function configurator.configure(ask_config) + tool_ctl.ask_config = ask_config == true + tool_ctl.has_config = settings.load("/rplc.settings") + + load_settings(ini_cfg) + + reset_term() -- set overridden colors for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end - -- init front panel view - local display = DisplayBox{window=term.current(),fg_bg=style.root} - _config_view(display) + pcall(function () + -- init front panel view + local display = DisplayBox{window=term.current(),fg_bg=style.root} + config_view(display) - local function clear() - display.delete() - term.setTextColor(colors.white) - term.setBackgroundColor(colors.black) - term.clear() - term.setCursorPos(1, 1) + while true do + local event, param1, param2, param3 = util.pull_event() + + -- handle event + if event == "timer" then + -- notify timer callback dispatcher if no other timer case claimed this event + tcd.handle(param1) + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then + -- handle a mouse event + local m_e = core.events.new_mouse_event(event, param1, param2, param3) + if m_e then display.handle_mouse(m_e) end + elseif event == "char" or event == "key" or event == "key_up" then + -- handle a key event + local k_e = core.events.new_key_event(event, param1, param2) + if k_e then display.handle_key(k_e) end + elseif event == "paste" then + -- handle a paste event + display.handle_paste(param1) + end + + -- check for termination request + if event == "terminate" then + return + end + end + end) + + -- 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 - while true do - local event, param1, param2, param3 = util.pull_event() - - -- handle event - if event == "timer" then - -- notify timer callback dispatcher if no other timer case claimed this event - tcd.handle(param1) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then - -- handle a mouse event - local m_e = core.events.new_mouse_event(event, param1, param2, param3) - if m_e then display.handle_mouse(m_e) end - elseif event == "char" or event == "key" or event == "key_up" then - -- handle a key event - local k_e = core.events.new_key_event(event, param1, param2) - if k_e then display.handle_key(k_e) end - elseif event == "paste" then - -- handle a paste event - display.handle_paste(param1) - end - - -- check for termination request - if event == "terminate" then - clear() - println("terminate requested, exiting config") - return false - end - end + reset_term() + println("exited configurator.") end return configurator From 02e9c09dafd0202605c89d1434cfaa9699d0861a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 15:30:49 -0400 Subject: [PATCH 28/35] #307 configurator error reporting --- reactor-plc/configure.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index e1da81c..b8ea0da 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -591,7 +591,7 @@ function configurator.configure(ask_config) term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end - pcall(function () + local status, error = pcall(function () -- init front panel view local display = DisplayBox{window=term.current(),fg_bg=style.root} config_view(display) @@ -630,7 +630,13 @@ function configurator.configure(ask_config) end reset_term() - println("exited configurator.") + if status then + println("exited configurator") + else + println("configurator error: " .. error) + end + + return status, error end return configurator From b1446637ad36cdf6402a6005b28f9db578407fd9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 17:06:24 -0400 Subject: [PATCH 29/35] checkbox default val and radio type checks for set_value --- graphics/elements/controls/checkbox.lua | 3 ++- graphics/elements/controls/radio_2d.lua | 2 +- graphics/elements/controls/radio_button.lua | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/checkbox.lua index 7f0dc58..08e8bc5 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -6,6 +6,7 @@ local element = require("graphics.element") ---@class checkbox_args ---@field label string checkbox text ---@field box_fg_bg cpair colors for checkbox +---@field default? boolean default value ---@field callback? function function to call on press ---@field parent graphics_element ---@field id? string element id @@ -28,7 +29,7 @@ local function checkbox(args) -- create new graphics element base object local e = element.new(args) - e.value = false + e.value = args.default == true -- show the button state local function draw() diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/radio_2d.lua index d87d87e..fde886f 100644 --- a/graphics/elements/controls/radio_2d.lua +++ b/graphics/elements/controls/radio_2d.lua @@ -182,7 +182,7 @@ local function radio_2d_button(args) -- set the value ---@param val integer new value function e.set_value(val) - if val > 0 and val <= #args.options then + if type(val) == "number" and val > 0 and val <= #args.options then e.value = val e.redraw() end diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index d54a1df..93540f3 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -126,7 +126,7 @@ local function radio_button(args) -- set the value ---@param val integer new value function e.set_value(val) - if val > 0 and val <= #args.options then + if type(val) == "number" and val > 0 and val <= #args.options then e.value = val e.redraw() end From bfa24b366558fa7e374c91b2f6040388e3ab12fa Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 17:10:16 -0400 Subject: [PATCH 30/35] #307 PLC integration with new config storage --- reactor-plc/panel/front_panel.lua | 4 +- reactor-plc/plc.lua | 82 +++++++++++++++++++------- reactor-plc/startup.lua | 97 +++++++++++++------------------ 3 files changed, 105 insertions(+), 78 deletions(-) diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index a59a016..cf04194 100644 --- a/reactor-plc/panel/front_panel.lua +++ b/reactor-plc/panel/front_panel.lua @@ -5,8 +5,8 @@ local types = require("scada-common.types") local util = require("scada-common.util") -local config = require("reactor-plc.config") local databus = require("reactor-plc.databus") +local plc = require("reactor-plc.plc") local style = require("reactor-plc.panel.style") @@ -86,7 +86,7 @@ local function init(panel) local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=ind_grn} -- only show emergency coolant LED if emergency coolant is configured for this device - if type(config.EMERGENCY_COOL) == "table" then + if plc.config.EmerCoolEnable then local emer_cool = LED{parent=status,x=2,width=14,label="EMER COOLANT",colors=cpair(colors.yellow,colors.yellow_off)} emer_cool.register(databus.ps, "emer_cool", emer_cool.update) end diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index bbb59ff..9140841 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -26,14 +26,58 @@ local RPS_LIMITS = const.RPS_LIMITS local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active." local PCALL_START_MSG = "pcall: Reactor is already active." +---@type plc_config +local config = {} + +plc.config = config + +-- load the PLC configuration +function plc.load_config() + config.Networked = settings.get("Networked") + config.UnitID = settings.get("UnitID") + config.EmerCoolEnable = settings.get("EmerCoolEnable") + config.EmerCoolSide = settings.get("EmerCoolSide") + config.EmerCoolColor = settings.get("EmerCoolColor") + config.SVR_Channel = settings.get("SVR_Channel") + config.PLC_Channel = settings.get("PLC_Channel") + config.ConnTimeout = settings.get("ConnTimeout") + config.TrustedRange = settings.get("TrustedRange") + config.AuthKey = settings.get("AuthKey") + config.LogMode = settings.get("LogMode") + config.LogPath = settings.get("LogPath") + config.LogDebug = settings.get("LogDebug") + + local cfv = util.new_validator() + + cfv.assert_type_bool(config.Networked) + cfv.assert_type_int(config.UnitID) + cfv.assert_type_bool(config.EmerCoolEnable) + cfv.assert_channel(config.SVR_Channel) + cfv.assert_channel(config.PLC_Channel) + cfv.assert_type_int(config.ConnTimeout) + cfv.assert_min(config.ConnTimeout, 2) + cfv.assert_type_num(config.TrustedRange) + cfv.assert_type_str(config.AuthKey) + cfv.assert_type_int(config.LogMode) + cfv.assert_type_str(config.LogPath) + cfv.assert_type_bool(config.LogDebug) + + -- check emergency coolant configuration if enabled + if config.EmerCoolEnable then + cfv.assert_eq(rsio.is_valid_side(config.EmerCoolSide), true) + cfv.assert_eq(config.EmerCoolColor == nil or rsio.is_color(config.EmerCoolColor), true) + end + + return cfv.valid() +end + -- RPS: Reactor Protection System
-- identifies dangerous states and SCRAMs reactor if warranted
-- autonomous from main SCADA supervisor/coordinator control ---@nodiscard ---@param reactor table ---@param is_formed boolean ----@param emer_cool nil|table emergency coolant configuration -function plc.rps_init(reactor, is_formed, emer_cool) +function plc.rps_init(reactor, is_formed) local state_keys = { high_dmg = 1, high_temp = 2, @@ -73,22 +117,22 @@ function plc.rps_init(reactor, is_formed, emer_cool) ---@param state boolean true to enable emergency coolant, false to disable local function _set_emer_cool(state) -- check if this was configured: if it's a table, fields have already been validated. - if type(emer_cool) == "table" then + if config.EmerCoolEnable then local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state) if level ~= false then - if rsio.is_color(emer_cool.color) then - local output = rs.getBundledOutput(emer_cool.side) + if rsio.is_color(config.EmerCoolColor) then + local output = rs.getBundledOutput(config.EmerCoolSide) if rsio.digital_write(level) then - output = colors.combine(output, emer_cool.color) + output = colors.combine(output, config.EmerCoolColor) else - output = colors.subtract(output, emer_cool.color) + output = colors.subtract(output, config.EmerCoolColor) end - rs.setBundledOutput(emer_cool.side, output) + rs.setBundledOutput(config.EmerCoolSide, output) else - rs.setOutput(emer_cool.side, rsio.digital_write(level)) + rs.setOutput(config.EmerCoolSide, rsio.digital_write(level)) end if state ~= self.emer_cool_active then @@ -443,16 +487,12 @@ end -- Reactor PLC Communications ---@nodiscard ----@param id integer reactor ID ---@param version string PLC version ---@param nic nic network interface device ----@param plc_channel integer PLC comms channel ----@param svr_channel integer supervisor server channel ----@param range integer trusted device connection range ---@param reactor table reactor device ---@param rps rps RPS reference ---@param conn_watchdog watchdog watchdog reference -function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, rps, conn_watchdog) +function plc.comms(version, nic, reactor, rps, conn_watchdog) local self = { sv_addr = comms.BROADCAST, seq_num = 0, @@ -466,13 +506,13 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r max_burn_rate = nil } - comms.set_trusted_range(range) + comms.set_trusted_range(config.TrustedRange) -- PRIVATE FUNCTIONS -- -- configure network channels nic.closeAll() - nic.open(plc_channel) + nic.open(config.PLC_Channel) -- send an RPLC packet ---@param msg_type RPLC_TYPE @@ -481,10 +521,10 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() - r_pkt.make(id, msg_type, msg) + r_pkt.make(config.UnitID, msg_type, msg) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) - nic.transmit(svr_channel, plc_channel, s_pkt) + nic.transmit(config.SVR_Channel, config.PLC_Channel, s_pkt) self.seq_num = self.seq_num + 1 end @@ -498,7 +538,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r m_pkt.make(msg_type, msg) s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - nic.transmit(svr_channel, plc_channel, s_pkt) + nic.transmit(config.SVR_Channel, config.PLC_Channel, s_pkt) self.seq_num = self.seq_num + 1 end @@ -673,7 +713,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r -- attempt to establish link with supervisor function public.send_link_req() - _send_mgmt(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, id }) + _send_mgmt(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, config.UnitID }) end -- send live status information @@ -769,7 +809,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r local src_addr = packet.scada_frame.src_addr() -- handle packets now that we have prints setup - if l_chan == plc_channel then + if l_chan == config.PLC_Channel then -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 97ac227..300fd16 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -4,58 +4,46 @@ 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") -local network = require("scada-common.network") -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 network = require("scada-common.network") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") -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 configure = require("reactor-plc.configure") +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.5.10" +local R_PLC_VERSION = "v1.6.0" local println = util.println local println_ts = util.println_ts ---------------------------------------- --- config validation +-- get configuration ---------------------------------------- -local cfv = util.new_validator() - -cfv.assert_type_bool(config.NETWORKED) -cfv.assert_type_int(config.REACTOR_ID) -cfv.assert_channel(config.SVR_CHANNEL) -cfv.assert_channel(config.PLC_CHANNEL) -cfv.assert_type_int(config.TRUSTED_RANGE) -cfv.assert_type_num(config.COMMS_TIMEOUT) -cfv.assert_min(config.COMMS_TIMEOUT, 2) -cfv.assert_type_str(config.LOG_PATH) -cfv.assert_type_int(config.LOG_MODE) - -assert(cfv.valid(), "bad config file: missing/invalid fields") - --- check emergency coolant configuration -if type(config.EMERGENCY_COOL) == "table" then - if not rsio.is_valid_side(config.EMERGENCY_COOL.side) then - assert(false, "bad config file: emergency coolant side unrecognized") - elseif config.EMERGENCY_COOL.color ~= nil and not rsio.is_color(config.EMERGENCY_COOL.color) then - assert(false, "bad config file: emergency coolant invalid redstone channel color provided") +if not plc.load_config() then + -- try to reconfigure (user action) + local success, error = configure.configure(true) + if success then + assert(plc.load_config(), "failed to load valid reactor PLC configuration") + else + assert(success, "reactor PLC configuration error: " .. error) end end +local config = plc.config + ---------------------------------------- -- log init ---------------------------------------- -log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) +log.init(config.LogPath, config.LogMode, config.LogDebug) log.info("========================================") log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) @@ -75,32 +63,32 @@ local function main() -- record firmware versions and ID databus.tx_versions(R_PLC_VERSION, comms.version) - databus.tx_id(config.REACTOR_ID) + databus.tx_id(config.UnitID) -- mount connected devices ppm.mount_all() -- message authentication init - if type(config.AUTH_KEY) == "string" then - network.init_mac(config.AUTH_KEY) + if string.len(config.AuthKey) > 0 then + network.init_mac(config.AuthKey) end -- shared memory across threads ---@class plc_shared_memory local __shared_memory = { -- networked setting - networked = config.NETWORKED, ---@type boolean + networked = config.Networked, -- PLC system state flags ---@class plc_state plc_state = { - init_ok = true, - fp_ok = false, - shutdown = false, - degraded = true, + init_ok = true, + fp_ok = false, + shutdown = false, + degraded = true, reactor_formed = true, - no_reactor = true, - no_modem = true + no_reactor = true, + no_modem = true }, -- control setpoints @@ -118,10 +106,10 @@ local function main() -- system objects plc_sys = { - rps = nil, ---@type rps - nic = nil, ---@type nic - plc_comms = nil, ---@type plc_comms - conn_watchdog = nil ---@type watchdog + rps = nil, ---@type rps + nic = nil, ---@type nic + plc_comms = nil, ---@type plc_comms + conn_watchdog = nil---@type watchdog }, -- message queues @@ -196,18 +184,17 @@ local function main() 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) + smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed) log.debug("init> rps init") if __shared_memory.networked then -- comms watchdog - smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) + smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) log.debug("init> conn watchdog started") -- create network interface then setup comms smem_sys.nic = network.nic(smem_dev.modem) - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_sys.nic, config.PLC_CHANNEL, config.SVR_CHANNEL, - config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) + smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) log.debug("init> comms init") else _println_no_fp("init> starting in offline mode") @@ -215,7 +202,7 @@ local function main() end -- notify user of emergency coolant configuration status - if config.EMERGENCY_COOL ~= nil then + if config.EmerCoolEnable then println("init> emergency coolant control ready") log.info("init> running with emergency coolant control available") end From 894229831d65a0957c47ffe6392905f25cd54da6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 17:12:59 -0400 Subject: [PATCH 31/35] #307 configure bugfixes and settings file rename --- reactor-plc/configure.lua | 112 ++++++++++++++++++++++++-------------- reactor-plc/plc.lua | 2 + 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index b8ea0da..5aaf047 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -71,7 +71,7 @@ local tool_ctl = { load_legacy = nil ---@type function } ----@class _plc_cfg +---@class plc_config local tmp_cfg = { Networked = false, UnitID = 0, @@ -88,7 +88,7 @@ local tmp_cfg = { LogDebug = false, } ----@class _plc_cfg +---@class plc_config local ini_cfg = {} local fields = { @@ -112,6 +112,25 @@ local side_options_map = { "top", "bottom", "left", "right", "front", "back" } local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" } local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown } +local color_name_map = { + [colors.red] = "red", + [colors.orange] = "orange", + [colors.yellow] = "yellow", + [colors.lime] = "lime", + [colors.green] = "green", + [colors.cyan] = "cyan", + [colors.lightBlue] = "lightBlue", + [colors.blue] = "blue", + [colors.purple] = "purple", + [colors.magenta] = "magenta", + [colors.pink] = "pink", + [colors.white] = "white", + [colors.lightGray] = "lightGray", + [colors.gray] = "gray", + [colors.black] = "black", + [colors.brown] = "brown" +} + -- convert text representation to index ---@param side string local function side_to_idx(side) @@ -120,8 +139,8 @@ local function side_to_idx(side) end end --- convert text representation to index ----@param color string +-- convert color to index +---@param color color local function color_to_idx(color) for k, v in ipairs(color_options_map) do if v == color then return k end @@ -129,7 +148,7 @@ local function color_to_idx(color) end -- load data from the settings file ----@param target _plc_cfg +---@param target plc_config local function load_settings(target) target.Networked = settings.get("Networked", false) target.UnitID = settings.get("UnitID", 1) @@ -171,13 +190,13 @@ local function config_view(display) -- MAIN PAGE - local y_offset = 0 + local y_start = 5 TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."} if tool_ctl.ask_config then - y_offset = 3 - TextBox{parent=main_page,x=2,y=5,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)} + TextBox{parent=main_page,x=2,y=y_start,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)} + y_start = y_start + 3 end local function view_config() @@ -188,12 +207,12 @@ local function config_view(display) end if fs.exists("/reactor-plc/config.lua") then - y_offset = y_offset + 5 - PushButton{parent=main_page,x=2,y=y_offset,min_width=28,text="Import Legacy 'config.lua'",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg} + PushButton{parent=main_page,x=2,y=y_start,min_width=28,text="Import Legacy 'config.lua'",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg} + y_start = y_start + 2 end - PushButton{parent=main_page,x=2,y=y_offset+2,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} - tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_offset+4,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} + PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure System",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end @@ -213,10 +232,15 @@ local function config_view(display) TextBox{parent=plc_c_1,x=1,y=1,height=1,text_align=CENTER,text="Would you like to set this PLC as networked?"} TextBox{parent=plc_c_1,x=1,y=3,height=4,text_align=CENTER,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=cpair(colors.gray,colors.lightGray)} - local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.set_networked(v)end} + local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)} + + local function submit_networked() + tool_ctl.set_networked(networked.get_value()) + plc_pane.set_value(2) + end PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=plc_c_2,x=1,y=1,height=1,text_align=CENTER,text="Please enter the reactor unit ID for this PLC."} TextBox{parent=plc_c_2,x=1,y=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)} @@ -241,7 +265,7 @@ local function config_view(display) TextBox{parent=plc_c_3,x=1,y=1,height=4,text_align=CENTER,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "} TextBox{parent=plc_c_3,x=1,y=6,height=5,text_align=CENTER,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=cpair(colors.gray,colors.lightGray)} - local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",box_fg_bg=cpair(colors.orange,colors.black),value=ini_cfg.EmerCoolEnable} + local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)} local function next_from_plc() if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(4) end @@ -256,12 +280,12 @@ local function config_view(display) PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} TextBox{parent=plc_c_4,x=1,y=1,height=1,text_align=CENTER,text="Emergency Coolant Redstone Output Side"} - local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,value=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange} + local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange} TextBox{parent=plc_c_4,x=1,y=5,height=1,text_align=CENTER,text="Bundled Redstone Configuration"} - local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end} - local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,value=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=cpair(colors.gray,colors.lightGray)} - color.disable() + local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end} + local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=cpair(colors.gray,colors.lightGray)} + if ini_cfg.EmerCoolColor == nil then color.disable() end local function submit_emcool() tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] @@ -326,12 +350,14 @@ local function config_view(display) local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local function submit_ct_tr() - if tonumber(timeout.get_value()) ~= nil and tonumber(range.get_value()) ~= nil then - tmp_cfg.ConnTimeout = timeout.get_value() - tmp_cfg.TrustedRange = range.get_value() + local timeout_val = tonumber(timeout.get_value()) + local range_val = tonumber(range.get_value()) + if timeout_val ~= nil and range_val ~= nil then + tmp_cfg.ConnTimeout = timeout_val + tmp_cfg.TrustedRange = range_val net_pane.set_value(3) p2_err.hide(true) - elseif tonumber(timeout.get_value()) == nil then + elseif timeout_val == nil then p2_err.set_value("Please set the connection timeout.") p2_err.show() else @@ -348,7 +374,13 @@ local function config_view(display) TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"} local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} - local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=function(v)censor(util.trinary(v,"*",nil))end} + + local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end + + local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + + hide_key.set_value(true) + censor_key(true) local function submit_auth() tmp_cfg.AuthKey = key.get_value() @@ -367,12 +399,12 @@ local function config_view(display) TextBox{parent=log_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."} TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,text="Log File Mode"} - local mode = RadioButton{parent=log_c_1,x=1,y=4,value=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} + local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink} TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"} local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=cpair(colors.black,colors.white)} - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,value=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} + local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)} local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} @@ -431,14 +463,15 @@ local function config_view(display) local function save_and_continue() for k, v in pairs(tmp_cfg) do settings.set(k, v) end - if settings.save("rplc.settings") then + if settings.save("reactor-plc.settings") then load_settings(tmp_cfg) try_set(networked, tmp_cfg.Networked) try_set(u_id, tmp_cfg.UnitID) try_set(en_em_cool, tmp_cfg.EmerCoolEnable) - try_set(side, tmp_cfg.EmerCoolSide) - try_set(color, tmp_cfg.EmerCoolColor) + try_set(side, side_to_idx(tmp_cfg.EmerCoolSide)) + try_set(bundled, tmp_cfg.EmerCoolColor ~= nil) + if tmp_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(tmp_cfg.EmerCoolColor)) end try_set(svr_chan, tmp_cfg.SVR_Channel) try_set(plc_chan, tmp_cfg.PLC_Channel) try_set(timeout, tmp_cfg.ConnTimeout) @@ -530,7 +563,7 @@ local function config_view(display) end -- generate the summary list - ---@param cfg _plc_cfg + ---@param cfg plc_config function tool_ctl.gen_summary(cfg) setting_list.remove_all() @@ -542,10 +575,12 @@ local function config_view(display) local height = 1 local label_w = string.len(f[2]) local val_max_w = (inner_width - label_w) + 1 - local val = util.strval(cfg[f[1]]) + local raw = cfg[f[1]] + local val = util.strval(raw) if f[1] == "AuthKey" and hide_key.get_value() then val = string.rep("*", string.len(val)) end - if f[1] == "LogMode" then val = util.trinary(cfg[f[1]] == log.MODE.APPEND, "append", "replace") end + if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end + if f[1] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end if val == "nil" then val = "n/a" end local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white)) @@ -580,7 +615,7 @@ end ---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration function configurator.configure(ask_config) tool_ctl.ask_config = ask_config == true - tool_ctl.has_config = settings.load("/rplc.settings") + tool_ctl.has_config = settings.load("/reactor-plc.settings") load_settings(ini_cfg) @@ -601,7 +636,7 @@ function configurator.configure(ask_config) -- handle event if event == "timer" then - -- notify timer callback dispatcher if no other timer case claimed this event + -- notify timer callback dispatcher tcd.handle(param1) elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event @@ -616,10 +651,7 @@ function configurator.configure(ask_config) display.handle_paste(param1) end - -- check for termination request - if event == "terminate" then - return - end + if event == "terminate" then return end end end) @@ -630,9 +662,7 @@ function configurator.configure(ask_config) end reset_term() - if status then - println("exited configurator") - else + if not status then println("configurator error: " .. error) end diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 9140841..6e62815 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -33,6 +33,8 @@ plc.config = config -- load the PLC configuration function plc.load_config() + assert(settings.load("/reactor-plc.settings"), "failed to load settings file, please reconfigure.") + config.Networked = settings.get("Networked") config.UnitID = settings.get("UnitID") config.EmerCoolEnable = settings.get("EmerCoolEnable") From b5e0183e5454fd1d960ab7802f78c687aa0d69fd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 Oct 2023 19:16:44 -0400 Subject: [PATCH 32/35] luacheck fix and added keys to luacheck globals --- .github/workflows/check.yml | 2 +- reactor-plc/configure.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 04db0a0..8524d55 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -28,4 +28,4 @@ jobs: # --no-max-line-length = Disable warnings for long line lengths # --exclude-files ... = Exclude lockbox library (external) and config files # --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os' - args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window + args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 5aaf047..1373a08 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -404,7 +404,7 @@ local function config_view(display) TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"} local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=cpair(colors.black,colors.white)} - local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black),callback=function(v)end} + local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)} local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} From ebabd99f2bada922bbfb6250d9dfeb558677220e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 Oct 2023 22:52:13 -0400 Subject: [PATCH 33/35] #307 fixes and cleanup --- graphics/core.lua | 10 +-- graphics/element.lua | 6 +- graphics/elements/controls/app.lua | 3 - graphics/elements/controls/radio_2d.lua | 4 +- graphics/elements/controls/radio_button.lua | 4 +- graphics/elements/form/number_field.lua | 2 +- graphics/elements/form/text_field.lua | 2 +- graphics/elements/indicators/power.lua | 2 +- graphics/elements/indicators/rad.lua | 2 +- graphics/elements/listbox.lua | 6 -- pocket/startup.lua | 2 +- reactor-plc/config.lua | 34 ---------- reactor-plc/configure.lua | 71 +++++++++++++-------- reactor-plc/plc.lua | 3 +- reactor-plc/startup.lua | 8 +-- reactor-plc/threads.lua | 2 +- rtu/threads.lua | 2 +- supervisor/startup.lua | 2 +- 18 files changed, 65 insertions(+), 100 deletions(-) delete mode 100644 reactor-plc/config.lua diff --git a/graphics/core.lua b/graphics/core.lua index 2f17d98..062bf60 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -115,8 +115,7 @@ end -- extract the custom element assert message, dropping the path to the element file function core.extract_assert_msg(msg) - local start = (string.find(msg, "@") + 1) or 1 - return string.sub(msg, start) + return string.sub(msg, (string.find(msg, "@") + 1) or 1) end -- Interactive Field Manager @@ -163,11 +162,8 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) function public.censor(censor) if type(censor) == "string" and string.len(censor) == 1 then self.censor = censor - public.show() - else - self.censor = nil - public.show() - end + else self.censor = nil end + public.show() end -- show the field diff --git a/graphics/element.lua b/graphics/element.lua index 2ace4e1..41e4c57 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -315,8 +315,8 @@ function element.new(args, child_offset_x, child_offset_y) -- defocus this element function protected.defocus() public.unfocus_all() end - -- request focus management to focus this element - function protected.req_focus() args.parent.__focus_child(public) end + -- focus this element and take away focus from all other elements + function protected.take_focus() args.parent.__focus_child(public) end -- action handlers -- @@ -426,7 +426,7 @@ function element.new(args, child_offset_x, child_offset_y) self.id = args.id or "__ROOT__" protected.prepare_template(0, 0, 1) else - self.id, self.ordinal = args.parent.__add_child(args.id, protected) + self.id = args.parent.__add_child(args.id, protected) end ---------------------- diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua index 1433193..f610393 100644 --- a/graphics/elements/controls/app.lua +++ b/graphics/elements/controls/app.lua @@ -118,11 +118,8 @@ local function app_button(args) -- element redraw function e.redraw() - -- write app title, centered e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4) e.w_write(args.title) - - -- draw button draw() end diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/radio_2d.lua index fde886f..65d4c09 100644 --- a/graphics/elements/controls/radio_2d.lua +++ b/graphics/elements/controls/radio_2d.lua @@ -188,11 +188,9 @@ local function radio_2d_button(args) end end - -- handle focus + -- handle focus & enable e.on_focused = e.redraw e.on_unfocused = e.redraw - - -- handle enable e.on_enabled = e.redraw e.on_disabled = e.redraw diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 93540f3..aee7060 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -132,11 +132,9 @@ local function radio_button(args) end end - -- handle focus + -- handle focus & enable e.on_focused = e.redraw e.on_unfocused = e.redraw - - -- handle enable e.on_enabled = e.redraw e.on_disabled = e.redraw diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index b519c8a..53fc473 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -48,7 +48,7 @@ local function number_field(args) -- only handle if on an increment or decrement arrow if e.enabled then if core.events.was_clicked(event.type) then - e.req_focus() + e.take_focus() if event.type == MOUSE_CLICK.UP then ifield.move_cursor(event.current.x) diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 5fc1062..aee910d 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -43,7 +43,7 @@ local function text_field(args) -- only handle if on an increment or decrement arrow if e.enabled then if core.events.was_clicked(event.type) then - e.req_focus() + e.take_focus() if event.type == MOUSE_CLICK.UP then ifield.move_cursor(event.current.x) diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua index 96cbfd5..69e4a0b 100644 --- a/graphics/elements/indicators/power.lua +++ b/graphics/elements/indicators/power.lua @@ -23,7 +23,7 @@ local element = require("graphics.element") ---@param args power_indicator_args ---@return graphics_element element, element_id id local function power(args) - element.assert(type(args.value) == "number", "value is a required number field") + element.assert(type(args.value) == "number", "value is a required field") element.assert(util.is_int(args.width), "width is a required field") args.height = 1 diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua index d13655e..545ea41 100644 --- a/graphics/elements/indicators/rad.lua +++ b/graphics/elements/indicators/rad.lua @@ -33,7 +33,7 @@ local function rad(args) -- create new graphics element base object local e = element.new(args) - e.value = types.new_zero_radiation_reading() + e.value = args.value or types.new_zero_radiation_reading() local label_len = string.len(args.label) local data_start = 1 diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index a35330a..d138e19 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/listbox.lua @@ -278,12 +278,6 @@ local function listbox(args) function e.redraw() draw_arrows(0) draw_bar() - - -- redraw all children - for i = 1, #list do - local item = list[i] ---@type listbox_item - item.e.redraw() - end end -- initial draw diff --git a/pocket/startup.lua b/pocket/startup.lua index 68fb2c1..4929955 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -170,7 +170,7 @@ local function main() -- got a packet local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5) pocket_comms.handle_packet(packet) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a monitor touch event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua deleted file mode 100644 index 066ccf6..0000000 --- a/reactor-plc/config.lua +++ /dev/null @@ -1,34 +0,0 @@ -local config = {} - --- set to false to run in offline mode (safety regulation only) -config.NETWORKED = true --- unique reactor ID -config.REACTOR_ID = 1 - --- for offline mode, this redstone interface will turn off (open a valve) --- when emergency coolant is needed due to low coolant --- config.EMERGENCY_COOL = { side = "right", color = nil } - --- supervisor comms channel -config.SVR_CHANNEL = 16240 --- PLC comms channel -config.PLC_CHANNEL = 16241 --- max trusted modem message distance (0 to disable check) -config.TRUSTED_RANGE = 0 --- time in seconds (>= 2) before assuming a remote device is no longer active -config.COMMS_TIMEOUT = 5 --- facility authentication key (do NOT use one of your passwords) --- this enables verifying that messages are authentic --- all devices on the same network must use the same key --- config.AUTH_KEY = "SCADAfacility123" - --- log path -config.LOG_PATH = "/log.txt" --- log mode --- 0 = APPEND (adds to existing file on start) --- 1 = NEW (replaces existing file on start) -config.LOG_MODE = 0 --- true to log verbose debug messages -config.LOG_DEBUG = false - -return config diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index 1373a08..c1b0481 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -16,8 +16,8 @@ local TextBox = require("graphics.elements.textbox") local CheckBox = require("graphics.elements.controls.checkbox") local PushButton = require("graphics.elements.controls.push_button") -local RadioButton = require("graphics.elements.controls.radio_button") local Radio2D = require("graphics.elements.controls.radio_2d") +local RadioButton = require("graphics.elements.controls.radio_button") local NumberField = require("graphics.elements.form.number_field") local TextField = require("graphics.elements.form.text_field") @@ -68,7 +68,12 @@ local tool_ctl = { bundled_emcool = nil, ---@type function gen_summary = nil, ---@type function show_current_cfg = nil, ---@type function - load_legacy = nil ---@type function + load_legacy = nil, ---@type function + + show_auth_key = nil, ---@type function + show_key_btn = nil, ---@type graphics_element + auth_key_textbox = nil, ---@type graphics_element + auth_key_value = "" } ---@class plc_config @@ -160,7 +165,7 @@ local function load_settings(target) target.ConnTimeout = settings.get("ConnTimeout", 5) target.TrustedRange = settings.get("TrustedRange", 0) target.AuthKey = settings.get("AuthKey", "") - target.LogMode = settings.get("LogMode", 0) + target.LogMode = settings.get("LogMode", log.MODE.APPEND) target.LogPath = settings.get("LogPath", "/log.txt") target.LogDebug = settings.get("LogDebug", false) end @@ -171,10 +176,8 @@ local function config_view(display) local nav_fg_bg = cpair(colors.black,colors.white) local btn_act_fg_bg = cpair(colors.white,colors.gray) - local function exit() ---@diagnostic disable-next-line: undefined-field - os.queueEvent("terminate") - end + local function exit() os.queueEvent("terminate") end TextBox{parent=display,y=1,text="Reactor PLC Configurator",alignment=CENTER,height=1,fg_bg=style.header} @@ -289,7 +292,7 @@ local function config_view(display) local function submit_emcool() tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] - tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil) + tmp_cfg.EmerCoolColor = color_options_map[color.get_value()] next_from_plc() end @@ -307,7 +310,7 @@ local function config_view(display) TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} TextBox{parent=net_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please set the network channels below."} - TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)} + TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)} TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"} local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)} @@ -464,22 +467,22 @@ local function config_view(display) for k, v in pairs(tmp_cfg) do settings.set(k, v) end if settings.save("reactor-plc.settings") then - load_settings(tmp_cfg) + load_settings(ini_cfg) - try_set(networked, tmp_cfg.Networked) - try_set(u_id, tmp_cfg.UnitID) - try_set(en_em_cool, tmp_cfg.EmerCoolEnable) - try_set(side, side_to_idx(tmp_cfg.EmerCoolSide)) - try_set(bundled, tmp_cfg.EmerCoolColor ~= nil) - if tmp_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(tmp_cfg.EmerCoolColor)) end - try_set(svr_chan, tmp_cfg.SVR_Channel) - try_set(plc_chan, tmp_cfg.PLC_Channel) - try_set(timeout, tmp_cfg.ConnTimeout) - try_set(range, tmp_cfg.TrustedRange) - try_set(key, tmp_cfg.AuthKey) - try_set(mode, tmp_cfg.LogMode) - try_set(path, tmp_cfg.LogPath) - try_set(en_dbg, tmp_cfg.LogDebug) + try_set(networked, ini_cfg.Networked) + try_set(u_id, ini_cfg.UnitID) + try_set(en_em_cool, ini_cfg.EmerCoolEnable) + try_set(side, side_to_idx(ini_cfg.EmerCoolSide)) + try_set(bundled, ini_cfg.EmerCoolColor ~= nil) + if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(plc_chan, ini_cfg.PLC_Channel) + try_set(timeout, ini_cfg.ConnTimeout) + try_set(range, ini_cfg.TrustedRange) + try_set(key, ini_cfg.AuthKey) + try_set(mode, ini_cfg.LogMode) + try_set(path, ini_cfg.LogPath) + try_set(en_dbg, ini_cfg.LogDebug) if tool_ctl.importing_legacy then tool_ctl.importing_legacy = false @@ -493,6 +496,7 @@ local function config_view(display) end PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)} tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"} @@ -554,7 +558,7 @@ local function config_view(display) tmp_cfg.AuthKey = config.AUTH_KEY or "" tmp_cfg.LogMode = config.LOG_MODE tmp_cfg.LogPath = config.LOG_PATH - tmp_cfg.LogDebug = config.LOG_DEBUG + tmp_cfg.LogDebug = config.LOG_DEBUG or false tool_ctl.gen_summary(tmp_cfg) sum_pane.set_value(1) @@ -562,6 +566,12 @@ local function config_view(display) tool_ctl.importing_legacy = true end + -- expose the auth key on the summary page + function tool_ctl.show_auth_key() + tool_ctl.show_key_btn.disable() + tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value) + end + -- generate the summary list ---@param cfg plc_config function tool_ctl.gen_summary(cfg) @@ -570,6 +580,9 @@ local function config_view(display) local alternate = false local inner_width = setting_list.get_width() - 1 + tool_ctl.show_key_btn.enable() + tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key + for i = 1, #fields do local f = fields[i] local height = 1 @@ -578,7 +591,7 @@ local function config_view(display) local raw = cfg[f[1]] local val = util.strval(raw) - if f[1] == "AuthKey" and hide_key.get_value() then val = string.rep("*", string.len(val)) end + if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end if f[1] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end if val == "nil" then val = "n/a" end @@ -594,11 +607,14 @@ local function config_view(display) local line = Div{parent=setting_list,height=height,fg_bg=c} TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + local textbox if height > 1 then - TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT} + textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT} else - TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} + textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} end + + if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end end end end @@ -627,7 +643,6 @@ function configurator.configure(ask_config) end local status, error = pcall(function () - -- init front panel view local display = DisplayBox{window=term.current(),fg_bg=style.root} config_view(display) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 0942bdc..02d7c3d 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -33,7 +33,7 @@ plc.config = config -- load the PLC configuration function plc.load_config() - assert(settings.load("/reactor-plc.settings"), "failed to load settings file, please reconfigure.") + if not settings.load("/reactor-plc.settings") then return false end config.Networked = settings.get("Networked") config.UnitID = settings.get("UnitID") @@ -59,6 +59,7 @@ function plc.load_config() cfv.assert_type_int(config.ConnTimeout) cfv.assert_min(config.ConnTimeout, 2) cfv.assert_type_num(config.TrustedRange) + cfv.assert_min(config.TrustedRange, 0) cfv.assert_type_str(config.AuthKey) cfv.assert_type_int(config.LogMode) cfv.assert_type_str(config.LogPath) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 300fd16..7ad0d27 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -106,10 +106,10 @@ local function main() -- system objects plc_sys = { - rps = nil, ---@type rps - nic = nil, ---@type nic - plc_comms = nil, ---@type plc_comms - conn_watchdog = nil---@type watchdog + rps = nil, ---@type rps + nic = nil, ---@type nic + plc_comms = nil, ---@type plc_comms + conn_watchdog = nil ---@type watchdog }, -- message queues diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index ccbc196..6699ea3 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -265,7 +265,7 @@ function threads.thread__main(smem, init) -- update indicators databus.tx_hw_status(plc_state) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) diff --git a/rtu/threads.lua b/rtu/threads.lua index 29e54cf..d76d062 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -279,7 +279,7 @@ function threads.thread__main(smem) end end end - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 11f0948..76fd746 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -213,7 +213,7 @@ local function main() -- got a packet local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) superv_comms.handle_packet(packet) - elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll"or + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) From 5d7a0b266aa4f78331273191d33975178d77b234 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 Oct 2023 23:11:52 -0400 Subject: [PATCH 34/35] handle new settings file and not deleting legacy config --- ccmsi.lua | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/ccmsi.lua b/ccmsi.lua index 6793515..1ec65e9 100644 --- a/ccmsi.lua +++ b/ccmsi.lua @@ -18,7 +18,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. local function println(message) print(tostring(message)) end local function print(message) term.write(tostring(message)) end -local CCMSI_VERSION = "v1.10" +local CCMSI_VERSION = "v1.11" local install_dir = "/.install-cache" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" @@ -158,7 +158,7 @@ local function _clean_dir(dir, tree) if fs.isDir(path) then _clean_dir(path, tree[val]) if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end - elseif not _in_array(val, tree) then + elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files fs.delete(path) println("deleted " .. path) end @@ -172,7 +172,7 @@ local function clean(manifest) table.insert(tree, "install_manifest.json") table.insert(tree, "ccmsi.lua") - table.insert(tree, "log.txt") -- this won't necessarily work correctly + table.insert(tree, "log.txt") ---@fixme fix after migration to settings files? lgray() @@ -203,8 +203,8 @@ if #opts == 0 or opts[1] == "help" then yellow() println(" ccmsi check for target") lgray() - println(" install - fresh install, overwrites config") - println(" update - update files EXCEPT for config/logs") + println(" install - fresh install, overwrites config.lua") + println(" update - update files EXCEPT for config.lua") println(" uninstall - delete files INCLUDING config/logs") white();println("");lgray() println(" reactor-plc - reactor PLC firmware") @@ -543,28 +543,38 @@ elseif mode == "uninstall" then local file_list = manifest.files local dependencies = manifest.depends[app] - local config_file = app .. "/config.lua" table.insert(dependencies, app) -- delete log file + local log_deleted = false + local settings_file = app .. ".settings" + local legacy_config_file = app .. "/config.lua" + lgray() - if fs.exists(config_file) then - local log_deleted = pcall(function () + if fs.exists(legacy_config_file) then + log_deleted = pcall(function () local config = require(app .. ".config") if fs.exists(config.LOG_PATH) then fs.delete(config.LOG_PATH) println("deleted log file " .. config.LOG_PATH) end end) - - if not log_deleted then - red();println("Failed to delete log file.") - white();println("press any key to continue...") - any_key();lgray() + elseif fs.exists(settings_file) and settings.load(settings_file) then + local log = settings.get("LogPath") + if log ~= nil and fs.exists(log) then + log_deleted = true + fs.delete(log) + println("deleted log file " .. log) end end + if not log_deleted then + red();println("Failed to delete log file.") + white();println("press any key to continue...") + any_key();lgray() + end + -- delete all installed files for _, dependency in pairs(dependencies) do local files = file_list[dependency] @@ -584,6 +594,11 @@ elseif mode == "uninstall" then end end + if fs.exists(settings_file) then + fs.delete(settings_file) + println("deleted " .. settings_file) + end + fs.delete("install_manifest.json") println("deleted install_manifest.json") From d2a1951b66771fb4bf6b2e12385c72a2f31a2db4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 Oct 2023 23:16:46 -0400 Subject: [PATCH 35/35] refactored TEXT_ALIGN to ALIGN --- coordinator/ui/components/imatrix.lua | 4 ++-- coordinator/ui/components/pkt_entry.lua | 4 ++-- coordinator/ui/components/process_ctl.lua | 8 ++++---- coordinator/ui/components/unit_detail.lua | 16 ++++++++-------- coordinator/ui/components/unit_flow.lua | 20 ++++++++++---------- coordinator/ui/components/unit_overview.lua | 4 ++-- coordinator/ui/layout/flow_view.lua | 16 ++++++++-------- coordinator/ui/layout/front_panel.lua | 8 ++++---- coordinator/ui/layout/main_view.lua | 8 ++++---- graphics/core.lua | 4 ++-- graphics/elements/textbox.lua | 10 +++++----- pocket/ui/components/conn_waiting.lua | 6 +++--- pocket/ui/main.lua | 4 ++-- pocket/ui/pages/boiler_page.lua | 4 ++-- pocket/ui/pages/diag_page.lua | 14 +++++++------- pocket/ui/pages/reactor_page.lua | 4 ++-- pocket/ui/pages/turbine_page.lua | 4 ++-- pocket/ui/pages/unit_page.lua | 4 ++-- reactor-plc/configure.lua | 6 +++--- reactor-plc/panel/front_panel.lua | 8 ++++---- rtu/panel/front_panel.lua | 8 ++++---- supervisor/panel/components/pdg_entry.lua | 4 ++-- supervisor/panel/components/rtu_entry.lua | 4 ++-- supervisor/panel/front_panel.lua | 10 +++++----- 24 files changed, 91 insertions(+), 91 deletions(-) diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index d15362c..fc2e038 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -16,7 +16,7 @@ local VerticalBar = require("graphics.elements.indicators.vbar") local cpair = core.cpair local border = core.border -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local text_fg_bg = style.text_colors local lu_col = style.lu_colors @@ -35,7 +35,7 @@ local function new_view(root, x, y, data, ps, id) local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y} TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=style.lg_gray} - TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray} + TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray} local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} diff --git a/coordinator/ui/components/pkt_entry.lua b/coordinator/ui/components/pkt_entry.lua index ac38a9c..d6ba4be 100644 --- a/coordinator/ui/components/pkt_entry.lua +++ b/coordinator/ui/components/pkt_entry.lua @@ -13,7 +13,7 @@ local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -33,7 +33,7 @@ local function init(parent, id) local ps_prefix = "pkt_" .. id .. "_" TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=text_fg_bg} - local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=text_fg_bg,nav_active=cpair(colors.gray,colors.black)} + local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=text_fg_bg,nav_active=cpair(colors.gray,colors.black)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=text_fg_bg} pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value) diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index d1ca1b0..efc6eb1 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -23,7 +23,7 @@ local HazardButton = require("graphics.elements.controls.hazard_button") local RadioButton = require("graphics.elements.controls.radio_button") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair local border = core.border @@ -246,8 +246,8 @@ local function new_view(root, x, y) mode.register(facility.ps, "process_mode", mode.set_value) local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg} - local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} - local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=gry_wht} + local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg} + local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht} stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value) stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value) @@ -328,7 +328,7 @@ local function new_view(root, x, y) local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1} TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn} - TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)} + TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)} local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index fb1eccd..64d5e5b 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -28,7 +28,7 @@ local PushButton = require("graphics.elements.controls.push_button") local RadioButton = require("graphics.elements.controls.radio_button") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair local border = core.border @@ -62,7 +62,7 @@ local function init(parent, id) local b_ps = unit.boiler_ps_tbl local t_ps = unit.turbine_ps_tbl - TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header} ----------------------------- -- main stats and core map -- @@ -134,8 +134,8 @@ local function init(parent, id) ------------------- local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg} - local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg} - local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=gry_wht} + local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg} + local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht} stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value) stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value) @@ -195,7 +195,7 @@ local function init(parent, id) -- RPS annunciator panel - TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=8} + TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8} local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9} local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1} @@ -223,7 +223,7 @@ local function init(parent, id) -- cooling annunciator panel - TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22} + TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22} local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23} local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1} local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7} @@ -382,7 +382,7 @@ local function init(parent, id) reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end) - TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48} + TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48} local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} @@ -470,7 +470,7 @@ local function init(parent, id) -- automatic control settings -- -------------------------------- - TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=TEXT_ALIGN.CENTER,width=13,height=1,x=32,y=36} + TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36} local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37} local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1} diff --git a/coordinator/ui/components/unit_flow.lua b/coordinator/ui/components/unit_flow.lua index b868f68..f3103e5 100644 --- a/coordinator/ui/components/unit_flow.lua +++ b/coordinator/ui/components/unit_flow.lua @@ -19,7 +19,7 @@ local DataIndicator = require("graphics.elements.indicators.data") local IndicatorLight = require("graphics.elements.indicators.light") local TriIndicatorLight = require("graphics.elements.indicators.trilight") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local sprintf = util.sprintf @@ -69,8 +69,8 @@ local function make(parent, x, y, wide, unit) ------------------ local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray} - TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=TEXT_ALIGN.CENTER,height=1} - TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1} + TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1} + TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1} TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray} @@ -109,8 +109,8 @@ local function make(parent, x, y, wide, unit) hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update) local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray} - TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=TEXT_ALIGN.CENTER,height=1} - TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=TEXT_ALIGN.CENTER,height=1} + TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1} + TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1} TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} @@ -128,8 +128,8 @@ local function make(parent, x, y, wide, unit) end local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray} - TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=TEXT_ALIGN.CENTER,height=1} - TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=TEXT_ALIGN.CENTER,height=1} + TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1} + TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1} TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray} for i = 1, unit.num_turbines do @@ -175,8 +175,8 @@ local function make(parent, x, y, wide, unit) local function _machine(mx, my, name) local l = string.len(name) + 2 - TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=TEXT_ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1} - TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=TEXT_ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1} + TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1} + TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1} end local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} @@ -203,7 +203,7 @@ local function make(parent, x, y, wide, unit) _machine(_wide(97, 83), 4, "PRC [Po] \x1a"); _machine(_wide(116, 94), 6, "SPENT WASTE \x1b") - TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} + TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg} local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn} local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7} diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua index b4a3750..17702e5 100644 --- a/coordinator/ui/components/unit_overview.lua +++ b/coordinator/ui/components/unit_overview.lua @@ -14,7 +14,7 @@ local Div = require("graphics.elements.div") local PipeNetwork = require("graphics.elements.pipenet") local TextBox = require("graphics.elements.textbox") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local pipe = core.pipe @@ -44,7 +44,7 @@ local function make(parent, x, y, unit) local root = Div{parent=parent,x=x,y=y,width=80,height=height} -- unit header message - TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header} ------------- -- REACTOR -- diff --git a/coordinator/ui/layout/flow_view.lua b/coordinator/ui/layout/flow_view.lua index ca90e3e..f19dda2 100644 --- a/coordinator/ui/layout/flow_view.lua +++ b/coordinator/ui/layout/flow_view.lua @@ -25,7 +25,7 @@ local StateIndicator = require("graphics.elements.indicators.state") local CONTAINER_MODE = types.CONTAINER_MODE -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair local border = core.border @@ -46,9 +46,9 @@ local function init(main) local tank_list = facility.tank_list -- window header message - local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.header} -- max length example: "01:23:45 AM - Wednesday, September 28 2022" - local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} + local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} datetime.register(facility.ps, "date_time", datetime.set_value) @@ -289,7 +289,7 @@ local function init(main) local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14} TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_gray} - TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.wh_gray} + TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray} local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12} @@ -339,7 +339,7 @@ local function init(main) local sps = Div{parent=main,x=140,y=3,height=12} TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_gray} - TextBox{parent=sps,text="SPS",alignment=TEXT_ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray} + TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray} local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10} @@ -361,13 +361,13 @@ local function init(main) -- statistics -- ---------------- - TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} + TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg} local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17} sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update) - TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} + TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg} local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17} local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17} @@ -377,7 +377,7 @@ local function init(main) po.register(facility.ps, "po_rate", po.update) popl.register(facility.ps, "po_pl_rate", popl.update) - TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} + TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg} local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17} diff --git a/coordinator/ui/layout/front_panel.lua b/coordinator/ui/layout/front_panel.lua index 1bfa046..ffd2a09 100644 --- a/coordinator/ui/layout/front_panel.lua +++ b/coordinator/ui/layout/front_panel.lua @@ -24,7 +24,7 @@ local TabBar = require("graphics.elements.controls.tabbar") local LED = require("graphics.elements.indicators.led") local RGBLED = require("graphics.elements.indicators.ledrgb") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -36,7 +36,7 @@ local led_grn = style.led_grn local function init(panel, num_units) local ps = iocontrol.get_db().fp.ps - TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.header} + TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp.header} local page_div = Div{parent=panel,x=1,y=3} @@ -90,8 +90,8 @@ local function init(panel, num_units) -- local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label} - 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} + local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1} + local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1} fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index 3bc9e99..64742c7 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -16,7 +16,7 @@ local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN -- create new main view ---@param main graphics_element main displaybox @@ -25,10 +25,10 @@ local function init(main) local units = iocontrol.get_db().units -- window header message - local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=style.header} local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=style.header} -- max length example: "01:23:45 AM - Wednesday, September 28 2022" - local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} + local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header} ping.register(facility.ps, "sv_ping", ping.update) datetime.register(facility.ps, "date_time", datetime.set_value) @@ -73,7 +73,7 @@ local function init(main) assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)") - TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.lg_gray} + TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray} cnc_bottom_align_start = cnc_bottom_align_start + 2 diff --git a/graphics/core.lua b/graphics/core.lua index 062bf60..7715f92 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -14,8 +14,8 @@ core.events = events -- Core Types ----@enum TEXT_ALIGN -core.TEXT_ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 } +---@enum ALIGN +core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 } ---@class graphics_border ---@field width integer diff --git a/graphics/elements/textbox.lua b/graphics/elements/textbox.lua index a6f7207..07a8736 100644 --- a/graphics/elements/textbox.lua +++ b/graphics/elements/textbox.lua @@ -5,11 +5,11 @@ local util = require("scada-common.util") local core = require("graphics.core") local element = require("graphics.element") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN ---@class textbox_args ---@field text string text to show ----@field alignment? TEXT_ALIGN text alignment, left by default +---@field alignment? ALIGN text alignment, left by default ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -31,7 +31,7 @@ local function textbox(args) e.value = args.text - local alignment = args.alignment or TEXT_ALIGN.LEFT + local alignment = args.alignment or ALIGN.LEFT -- draw textbox function e.redraw() @@ -45,9 +45,9 @@ local function textbox(args) local len = string.len(lines[i]) -- use cursor position to align this line - if alignment == TEXT_ALIGN.CENTER then + if alignment == ALIGN.CENTER then e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i) - elseif alignment == TEXT_ALIGN.RIGHT then + elseif alignment == ALIGN.RIGHT then e.w_set_cur((e.frame.w - len) + 1, i) else e.w_set_cur(1, i) diff --git a/pocket/ui/components/conn_waiting.lua b/pocket/ui/components/conn_waiting.lua index 114d165..9cbe1b3 100644 --- a/pocket/ui/components/conn_waiting.lua +++ b/pocket/ui/components/conn_waiting.lua @@ -11,7 +11,7 @@ local TextBox = require("graphics.elements.textbox") local WaitingAnim = require("graphics.elements.animations.waiting") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -29,10 +29,10 @@ local function init(parent, y, is_api) if is_api then WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} - TextBox{parent=box,text="Connecting to API",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} + TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} else WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} - TextBox{parent=box,text="Connecting to Supervisor",alignment=TEXT_ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} + TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,height=1,fg_bg=cpair(colors.white,style.root.bkg)} end return root diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 212b5bc..4cd70c6 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -26,7 +26,7 @@ local Sidebar = require("graphics.elements.controls.sidebar") local LINK_STATE = iocontrol.LINK_STATE local NAV_PAGE = iocontrol.NAV_PAGE -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -37,7 +37,7 @@ local function init(main) local ps = iocontrol.get_db().ps -- window header message - TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header} + TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header} -- -- root panel panes (connection screens + main screen) diff --git a/pocket/ui/pages/boiler_page.lua b/pocket/ui/pages/boiler_page.lua index ec13d59..ff92b05 100644 --- a/pocket/ui/pages/boiler_page.lua +++ b/pocket/ui/pages/boiler_page.lua @@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox") -- local cpair = core.cpair -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN -- new boiler page view ---@param root graphics_element parent local function new_view(root) local main = Div{parent=root,x=1,y=1} - TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=ALIGN.CENTER} return main end diff --git a/pocket/ui/pages/diag_page.lua b/pocket/ui/pages/diag_page.lua index 38d9368..c28aba9 100644 --- a/pocket/ui/pages/diag_page.lua +++ b/pocket/ui/pages/diag_page.lua @@ -17,7 +17,7 @@ local cpair = core.cpair local NAV_PAGE = iocontrol.NAV_PAGE -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN -- new diagnostics page view ---@param root graphics_element parent @@ -28,7 +28,7 @@ local function new_view(root) local diag_home = Div{parent=main,x=1,y=1} - TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER} local alarm_test = Div{parent=main,x=1,y=1} @@ -63,15 +63,15 @@ local function new_view(root) local audio = Div{parent=alarm_test,x=1,y=1} - TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=ALIGN.CENTER} - ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)} + ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)} PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag} local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)} - TextBox{parent=tones,text="Tones",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()} + TextBox{parent=tones,text="Tones",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()} local test_btns = {} test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1} @@ -94,7 +94,7 @@ local function new_view(root) local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)} - TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()} + TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()} local alarm_btns = {} alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach} @@ -121,7 +121,7 @@ local function new_view(root) local states = Div{parent=audio,x=2,y=14,height=5,width=8} - TextBox{parent=states,text="States",height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=states,text="States",height=1,alignment=ALIGN.CENTER} local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray} local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray} local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray} diff --git a/pocket/ui/pages/reactor_page.lua b/pocket/ui/pages/reactor_page.lua index c7a2e96..ae11436 100644 --- a/pocket/ui/pages/reactor_page.lua +++ b/pocket/ui/pages/reactor_page.lua @@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox") -- local cpair = core.cpair -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN -- new reactor page view ---@param root graphics_element parent local function new_view(root) local main = Div{parent=root,x=1,y=1} - TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=ALIGN.CENTER} return main end diff --git a/pocket/ui/pages/turbine_page.lua b/pocket/ui/pages/turbine_page.lua index 527e419..41d656c 100644 --- a/pocket/ui/pages/turbine_page.lua +++ b/pocket/ui/pages/turbine_page.lua @@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox") -- local cpair = core.cpair -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN -- new turbine page view ---@param root graphics_element parent local function new_view(root) local main = Div{parent=root,x=1,y=1} - TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=ALIGN.CENTER} return main end diff --git a/pocket/ui/pages/unit_page.lua b/pocket/ui/pages/unit_page.lua index a0718e6..de61e81 100644 --- a/pocket/ui/pages/unit_page.lua +++ b/pocket/ui/pages/unit_page.lua @@ -7,14 +7,14 @@ local TextBox = require("graphics.elements.textbox") -- local cpair = core.cpair -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN -- new unit page view ---@param root graphics_element parent local function new_view(root) local main = Div{parent=root,x=1,y=1} - TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER} + TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER} return main end diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index c1b0481..7ad98a3 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -26,9 +26,9 @@ local println = util.println local cpair = core.cpair -local LEFT = core.TEXT_ALIGN.LEFT -local CENTER = core.TEXT_ALIGN.CENTER -local RIGHT = core.TEXT_ALIGN.RIGHT +local LEFT = core.ALIGN.LEFT +local CENTER = core.ALIGN.CENTER +local RIGHT = core.ALIGN.RIGHT ---@class plc_configurator local configurator = {} diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index cf04194..41737e8 100644 --- a/reactor-plc/panel/front_panel.lua +++ b/reactor-plc/panel/front_panel.lua @@ -23,7 +23,7 @@ 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.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair local border = core.border @@ -34,7 +34,7 @@ local ind_red = style.ind_red -- create new front panel view ---@param panel graphics_element main displaybox local function init(panel) - local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,height=1,fg_bg=style.header} header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end) -- @@ -108,8 +108,8 @@ local function init(panel) -- 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} + local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1} + local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1} fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index 1bf2f51..2e5537b 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -18,7 +18,7 @@ local DataIndicator = require("graphics.elements.indicators.data") local LED = require("graphics.elements.indicators.led") local RGBLED = require("graphics.elements.indicators.ledrgb") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -32,7 +32,7 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC ---@param panel graphics_element 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} + TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,height=1,fg_bg=style.header} -- -- system indicators @@ -75,8 +75,8 @@ local function init(panel, units) -- local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=fp_label} - 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} + local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1} + local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1} fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) diff --git a/supervisor/panel/components/pdg_entry.lua b/supervisor/panel/components/pdg_entry.lua index 3c6ec43..6cb07f7 100644 --- a/supervisor/panel/components/pdg_entry.lua +++ b/supervisor/panel/components/pdg_entry.lua @@ -13,7 +13,7 @@ local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -31,7 +31,7 @@ local function init(parent, id) local ps_prefix = "pdg_" .. id .. "_" TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=black_lg} - local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=black_lg,nav_active=cpair(colors.gray,colors.black)} + local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=black_lg,nav_active=cpair(colors.gray,colors.black)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=black_lg} pdg_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value) diff --git a/supervisor/panel/components/rtu_entry.lua b/supervisor/panel/components/rtu_entry.lua index b6c1324..240c596 100644 --- a/supervisor/panel/components/rtu_entry.lua +++ b/supervisor/panel/components/rtu_entry.lua @@ -13,7 +13,7 @@ local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -31,7 +31,7 @@ local function init(parent, id) local ps_prefix = "rtu_" .. id .. "_" TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=black_lg} - local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=black_lg,nav_active=cpair(colors.gray,colors.black)} + local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=black_lg,nav_active=cpair(colors.gray,colors.black)} TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=black_lg} rtu_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value) diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua index 3655dc4..981dd5e 100644 --- a/supervisor/panel/front_panel.lua +++ b/supervisor/panel/front_panel.lua @@ -25,7 +25,7 @@ local TabBar = require("graphics.elements.controls.tabbar") local LED = require("graphics.elements.indicators.led") local DataIndicator = require("graphics.elements.indicators.data") -local TEXT_ALIGN = core.TEXT_ALIGN +local ALIGN = core.ALIGN local cpair = core.cpair @@ -40,7 +40,7 @@ local ind_grn = style.ind_grn -- create new front panel view ---@param panel graphics_element main displaybox local function init(panel) - TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} + TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.header} local page_div = Div{parent=panel,x=1,y=3} @@ -73,8 +73,8 @@ local function init(panel) -- local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label} - 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} + local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1} + local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1} fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) @@ -93,7 +93,7 @@ local function init(panel) local plc_entry = Div{parent=plc_list,height=3,fg_bg=bw_fg_bg} TextBox{parent=plc_entry,x=1,y=1,text="",width=8,height=1,fg_bg=black_lg} - TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=black_lg} + TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=ALIGN.CENTER,width=8,height=1,fg_bg=black_lg} TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=black_lg} local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=ind_grn}