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/.vscode/settings.json b/.vscode/settings.json index 673ad4e..b5e7976 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "colors", "fs", "http", + "keys", "parallel", "periphemu", "peripheral", @@ -24,6 +25,7 @@ }, "Lua.hint.setType": true, "Lua.diagnostics.disable": [ - "duplicate-set-field" + "duplicate-set-field", + "inject-field" ] } diff --git a/ccmsi.lua b/ccmsi.lua index fbac97e..8b8fa6d 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.9a" +local CCMSI_VERSION = "v1.11a" 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") + table.insert(tree, "log.txt") ---@fixme fix after migration to settings files? lgray() @@ -181,7 +181,7 @@ local function clean(manifest) if fs.isDir(val) then if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end - elseif not _in_array(val, tree) then + elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then root_ext = true yellow();println(val .. " not used") end @@ -203,10 +203,9 @@ 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(" remove - delete files EXCEPT for config/logs") - println(" purge - delete files INCLUDING 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") println(" rtu - RTU firmware") @@ -218,7 +217,7 @@ if #opts == 0 or opts[1] == "help" then lgray();println(" main (default) | latest | devel");white() return else - mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" }) + mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) if mode == nil then red();println("Unrecognized mode.");white() return @@ -310,7 +309,7 @@ elseif mode == "install" or mode == "update" then ver.lockbox.v_local = lmnf.versions.lockbox if lmnf.versions[app] == nil then - red();println("Another application is already installed, please purge it before installing a new application.");white() + red();println("Another application is already installed, please uninstall it before installing a new application.");white() return end end @@ -389,9 +388,10 @@ elseif mode == "install" or mode == "update" then -- check space constraints if space_available < space_required then single_file_mode = true - yellow();println("WARNING: Insufficient space available for a full download!");white() - println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.") - if mode == "update" then println("If installation still fails, delete this device's log file or uninstall the app (not purge) and try again.") end + yellow();println("NOTICE: Insufficient space available for a full cached download!");white() + lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.") + if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end + white(); if not ask_y_n("Do you wish to continue", false) then println("Operation cancelled.") return @@ -521,22 +521,19 @@ elseif mode == "install" or mode == "update" then else println("Update failed, files may have been skipped.") end end end -elseif mode == "remove" or mode == "purge" then +elseif mode == "uninstall" then local ok, manifest = read_local_manifest() if not ok then red();println("Error parsing local installation manifest.");white() return - elseif mode == "remove" and manifest.versions[app] == nil then - red();println(app .. " is not installed, cannot remove.");white() + end + + if manifest.versions[app] == nil then + red();println("Error: '" .. app .. "' is not installed.") return end - orange() - if mode == "remove" then - println("Removing all " .. app .. " files except for config and log...") - elseif mode == "purge" then - println("Purging all " .. app .. " files including config and log...") - end + orange();println("Uninstalling all " .. app .. " files...") -- ask for confirmation if not ask_y_n("Continue", false) then return end @@ -546,76 +543,65 @@ elseif mode == "remove" or mode == "purge" then local file_list = manifest.files local dependencies = manifest.depends[app] - local config_file = app .. "/config.lua" table.insert(dependencies, app) - -- delete log file if purging + -- delete log file + local log_deleted = false + local settings_file = app .. ".settings" + local legacy_config_file = app .. "/config.lua" + lgray() - if mode == "purge" and 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 - -- delete all files except config unless purging + 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] for _, file in pairs(files) do - if mode == "purge" or file ~= config_file then - if fs.exists(file) then fs.delete(file);println("deleted " .. file) end - end + if fs.exists(file) then fs.delete(file);println("deleted " .. file) end end - -- delete folders that we should be deleteing - if mode == "purge" or dependency ~= app then - local folder = files[1] - while true do - local dir = fs.getDir(folder) - if dir == "" or dir == ".." then break else folder = dir end - end + local folder = files[1] + while true do + local dir = fs.getDir(folder) + if dir == "" or dir == ".." then break else folder = dir end + end - if fs.isDir(folder) then - fs.delete(folder) - println("deleted directory " .. folder) - end - elseif dependency == app then - -- delete individual subdirectories so we can leave the config - for _, folder in pairs(files) do - while true do - local dir = fs.getDir(folder) - if dir == "" or dir == ".." or dir == app then break else folder = dir end - end - - if folder ~= app and fs.isDir(folder) then - fs.delete(folder);println("deleted app subdirectory " .. folder) - end - end + if fs.isDir(folder) then + fs.delete(folder) + println("deleted directory " .. folder) end end - -- only delete manifest if purging - if mode == "purge" then - fs.delete("install_manifest.json") - println("deleted install_manifest.json") - else - -- remove all data from versions list to show nothing is installed - manifest.versions = {} - local imfile = fs.open("install_manifest.json", "w") - imfile.write(textutils.serializeJSON(manifest)) - imfile.close() + 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") + green();println("Done!") end diff --git a/configure.lua b/configure.lua new file mode 100644 index 0000000..5234f1d --- /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").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/coordinator/coordinator.lua b/coordinator/coordinator.lua index 93f39b1..80c21b6 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -17,8 +17,8 @@ local println = util.println local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE -local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE +local MGMT_TYPE = comms.MGMT_TYPE +local CRDN_TYPE = comms.CRDN_TYPE local UNIT_COMMAND = comms.UNIT_COMMAND local FAC_COMMAND = comms.FAC_COMMAND @@ -54,9 +54,9 @@ end function coordinator.configure_monitors(num_units, disable_flow_view) ---@class monitors_struct local monitors = { - primary = nil, + primary = nil, ---@type table|nil primary_name = "", - flow = nil, + flow = nil, ---@type table|nil flow_name = "", unit_displays = {}, unit_name_map = {} @@ -279,7 +279,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk apisessions.init(nic) -- send a packet to the supervisor - ---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE + ---@param msg_type MGMT_TYPE|CRDN_TYPE ---@param msg table local function _send_sv(protocol, msg_type, msg) local s_pkt = comms.scada_packet() @@ -307,7 +307,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack }) + m_pkt.make(MGMT_TYPE.ESTABLISH, { ack }) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) nic.transmit(pkt_channel, crd_channel, s_pkt) @@ -316,13 +316,13 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk -- attempt connection establishment local function _send_establish() - _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN }) + _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD }) end -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) - _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) + _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- PUBLIC FUNCTIONS -- @@ -394,20 +394,20 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk self.sv_linked = false self.sv_r_seq_num = nil iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) - _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) + _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {}) end -- send a facility command ---@param cmd FAC_COMMAND command ---@param option any? optional option options for the optional options (like waste mode) function public.send_fac_command(cmd, option) - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option }) + _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option }) end -- send the auto process control configuration with a start command ---@param config coord_auto_config configuration function public.send_auto_start(config) - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { + _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits }) end @@ -417,7 +417,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk ---@param unit integer unit ID ---@param option any? optional option options for the optional options (like burn rate) function public.send_unit_command(cmd, unit, option) - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) + _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) end -- parse a packet @@ -426,7 +426,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk ---@param reply_to integer ---@param message any ---@param distance integer - ---@return mgmt_frame|crdn_frame|capi_frame|nil packet + ---@return mgmt_frame|crdn_frame|nil packet function public.parse_packet(side, sender, reply_to, message, distance) local s_pkt = nic.receive(side, sender, reply_to, message, distance) local pkt = nil @@ -444,12 +444,6 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk if crdn_pkt.decode(s_pkt) then pkt = crdn_pkt.get() end - -- get as coordinator API packet - elseif s_pkt.protocol() == PROTOCOL.COORD_API then - local capi_pkt = comms.capi_packet() - if capi_pkt.decode(s_pkt) then - pkt = capi_pkt.get() - end else log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) end @@ -459,7 +453,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk end -- handle a packet - ---@param packet mgmt_frame|crdn_frame|capi_frame|nil + ---@param packet mgmt_frame|crdn_frame|nil ---@return boolean close_ui function public.handle_packet(packet) local was_linked = self.sv_linked @@ -475,18 +469,18 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk elseif r_chan == pkt_channel then if not self.sv_linked then log.debug("discarding pocket API packet before linked to supervisor") - elseif protocol == PROTOCOL.COORD_API then - ---@cast packet capi_frame + elseif protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame -- look for an associated session local session = apisessions.find_session(src_addr) - -- API packet + -- coordinator packet if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) else -- any other packet should be session related, discard it - log.debug("discarding COORD_API packet without a known session") + log.debug("discarding SCADA_CRDN packet without a known session") end elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame @@ -497,7 +491,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session -- validate packet and continue if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then @@ -553,7 +547,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk if protocol == PROTOCOL.SCADA_CRDN then ---@cast packet crdn_frame if self.sv_linked then - if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then + if packet.type == CRDN_TYPE.INITIAL_BUILDS then if packet.length == 2 then -- record builds local fac_builds = iocontrol.record_facility_builds(packet.data[1]) @@ -561,31 +555,31 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk if fac_builds and unit_builds then -- acknowledge receipt of builds - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {}) + _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {}) else log.debug("received invalid INITIAL_BUILDS packet") end else log.debug("INITIAL_BUILDS packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then + elseif packet.type == CRDN_TYPE.FAC_BUILDS then if packet.length == 1 then -- record facility builds if iocontrol.record_facility_builds(packet.data[1]) then -- acknowledge receipt of builds - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {}) + _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {}) else log.debug("received invalid FAC_BUILDS packet") end else log.debug("FAC_BUILDS packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then + elseif packet.type == CRDN_TYPE.FAC_STATUS then -- update facility status if not iocontrol.update_facility_status(packet.data) then log.debug("received invalid FAC_STATUS packet") end - elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then + elseif packet.type == CRDN_TYPE.FAC_CMD then -- facility command acknowledgement if packet.length >= 2 then local cmd = packet.data[1] @@ -613,24 +607,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk else log.debug("SCADA_CRDN facility command ack packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then + elseif packet.type == CRDN_TYPE.UNIT_BUILDS then -- record builds if packet.length == 1 then if iocontrol.record_unit_builds(packet.data[1]) then -- acknowledge receipt of builds - _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {}) + _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {}) else log.debug("received invalid UNIT_BUILDS packet") end else log.debug("UNIT_BUILDS packet length mismatch") end - elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then + elseif packet.type == CRDN_TYPE.UNIT_STATUSES then -- update statuses if not iocontrol.update_unit_statuses(packet.data) then log.debug("received invalid UNIT_STATUSES packet") end - elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then + elseif packet.type == CRDN_TYPE.UNIT_CMD then -- unit command acknowledgement if packet.length == 3 then local cmd = packet.data[1] @@ -672,7 +666,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame if self.sv_linked then - if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then local timestamp = packet.data[1] @@ -690,7 +684,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk else log.debug("SCADA keep alive packet length mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.CLOSE then + elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close sv_watchdog.cancel() self.sv_addr = comms.BROADCAST @@ -701,7 +695,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk else log.debug("received unknown SCADA_MGMT packet type " .. packet.type) end - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with supervisor established if packet.length == 2 then local est_ack = packet.data[1] diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index d64ca98..5aadbb2 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 @@ -131,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 @@ -176,36 +189,47 @@ function renderer.close_fp() end end --- start the coordinator GUI -function renderer.start_ui() +-- try to start the main GUI +---@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/session/pocket.lua b/coordinator/session/pocket.lua index f5211a7..a19f7c3 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -8,8 +8,8 @@ local iocontrol = require("coordinator.iocontrol") local pocket = {} local PROTOCOL = comms.PROTOCOL --- local CAPI_TYPE = comms.CAPI_TYPE -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +-- local CRDN_TYPE = comms.CRDN_TYPE +local MGMT_TYPE = comms.MGMT_TYPE -- retry time constants in ms -- local INITIAL_WAIT = 1500 @@ -72,22 +72,22 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) iocontrol.fp_pkt_disconnected(id) end - -- send a CAPI packet - -----@param msg_type CAPI_TYPE + -- send a CRDN packet + -----@param msg_type CRDN_TYPE -----@param msg table -- local function _send(msg_type, msg) -- local s_pkt = comms.scada_packet() - -- local c_pkt = comms.capi_packet() + -- local c_pkt = comms.crdn_packet() -- c_pkt.make(msg_type, msg) - -- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, c_pkt.raw_sendable()) + -- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) -- out_queue.push_packet(s_pkt) -- self.seq_num = self.seq_num + 1 -- end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() @@ -101,7 +101,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) end -- handle a packet - ---@param pkt mgmt_frame|capi_frame + ---@param pkt mgmt_frame|crdn_frame local function _handle_packet(pkt) -- check sequence number if self.r_seq_num == nil then @@ -117,17 +117,17 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) self.conn_watchdog.feed() -- process packet - if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then - ---@cast pkt capi_frame + if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then + ---@cast pkt crdn_frame -- handle packet by type if pkt.type == nil then else - log.debug(log_header .. "handler received unsupported CAPI packet type " .. pkt.type) + log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type) end elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then ---@cast pkt mgmt_frame - if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if pkt.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -146,7 +146,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then + elseif pkt.type == MGMT_TYPE.CLOSE then -- close the session _close() else @@ -174,7 +174,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + _send_mgmt(MGMT_TYPE.CLOSE, {}) log.info(log_header .. "session closed by server") end @@ -229,7 +229,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) + _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end diff --git a/coordinator/startup.lua b/coordinator/startup.lua index b4f08ce..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.9" +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)) @@ -198,9 +197,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 @@ -358,7 +356,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/coordinator/ui/components/boiler.lua b/coordinator/ui/components/boiler.lua index 114a0bb..0d651d9 100644 --- a/coordinator/ui/components/boiler.lua +++ b/coordinator/ui/components/boiler.lua @@ -12,20 +12,19 @@ local VerticalBar = require("graphics.elements.indicators.vbar") local cpair = core.cpair local border = core.border +local text_fg_bg = style.text_colors + -- new boiler view ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y ---@param ps psil ps interface local function new_view(root, x, y, ps) - local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y} - - local text_fg_bg = cpair(colors.black, colors.lightGray) - local lu_col = cpair(colors.gray, colors.gray) + local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y} local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12} - local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg} - local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} + local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=style.lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg} + local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} status.register(ps, "computed_status", status.update) temp.register(ps, "temperature", temp.update) diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index 2d553a5..fc2e038 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -16,7 +16,10 @@ 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 -- new induction matrix view ---@param root graphics_element parent @@ -31,14 +34,12 @@ 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=cpair(colors.lightGray,colors.gray)} - TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.gray)} + TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,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} - local text_fg_bg = cpair(colors.black, colors.lightGray) local label_fg_bg = cpair(colors.gray, colors.lightGray) - local lu_col = cpair(colors.gray, colors.gray) local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg} diff --git a/coordinator/ui/components/pkt_entry.lua b/coordinator/ui/components/pkt_entry.lua index 8ba805c..d6ba4be 100644 --- a/coordinator/ui/components/pkt_entry.lua +++ b/coordinator/ui/components/pkt_entry.lua @@ -4,6 +4,8 @@ local iocontrol = require("coordinator.iocontrol") +local style = require("coordinator.ui.style") + local core = require("graphics.core") local Div = require("graphics.elements.div") @@ -11,10 +13,13 @@ 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 +local text_fg_bg = style.text_colors +local lg_wh = style.lg_white + -- create a pocket list entry ---@param parent graphics_element parent ---@param id integer PKT session ID @@ -23,22 +28,22 @@ local function init(parent, id) -- root div local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} - local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} + local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg} local ps_prefix = "pkt_" .. id .. "_" - TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} - TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + 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=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) TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} - local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_wh} pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value) TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1} - local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_wh} + TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_wh} pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update) pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor) diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index a8a46e7..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 @@ -33,6 +33,8 @@ local lu_cpair = style.lu_colors local hzd_fg_bg = style.hzd_fg_bg local dis_colors = style.dis_colors +local gry_wht = style.gray_white + local ind_grn = style.ind_grn local ind_yel = style.ind_yel local ind_red = style.ind_red @@ -47,6 +49,10 @@ local period = core.flasher.PERIOD local function new_view(root, x, y) assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)") + local black = cpair(colors.black, colors.black) + local blk_brn = cpair(colors.black, colors.brown) + local blk_pur = cpair(colors.black, colors.purple) + local facility = iocontrol.get_db().facility local units = iocontrol.get_db().units @@ -116,35 +122,35 @@ local function new_view(root, x, y) local targets = Div{parent=proc,width=31,height=24,x=1,y=1} - local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} + local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur} TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2} - local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} - local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht} + local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg} TextBox{parent=burn_target,x=18,y=2,text="mB/t"} - local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} + local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} b_target.register(facility.ps, "process_burn_target", b_target.set_value) burn_sum.register(facility.ps, "burn_sum", burn_sum.update) - local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} + local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur} TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2} - local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} - local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht} + local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg} TextBox{parent=chg_target,x=18,y=2,text="MFE"} - local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} + local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} c_target.register(facility.ps, "process_charge_target", c_target.set_value) cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end) - local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)} + local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur} TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} - local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)} - local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht} + local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg} TextBox{parent=gen_target,x=18,y=2,text="kFE/t"} - local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)} + local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} g_target.register(facility.ps, "process_gen_target", g_target.set_value) cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end) @@ -159,10 +165,10 @@ local function new_view(root, x, y) for i = 1, 4 do local unit - local tag_fg_bg = cpair(colors.gray,colors.white) - local lim_fg_bg = cpair(colors.lightGray,colors.white) + local tag_fg_bg = gry_wht + local lim_fg_bg = style.lg_white local ctl_fg = colors.lightGray - local cur_fg_bg = cpair(colors.lightGray,colors.white) + local cur_fg_bg = style.lg_white local cur_lu = colors.lightGray if i <= facility.num_units then @@ -170,7 +176,7 @@ local function new_view(root, x, y) tag_fg_bg = cpair(colors.black,colors.lightBlue) lim_fg_bg = bw_fg_bg ctl_fg = colors.gray - cur_fg_bg = cpair(colors.black,colors.brown) + cur_fg_bg = blk_brn cur_lu = colors.black end @@ -180,7 +186,7 @@ local function new_view(root, x, y) TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)} - local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=lim_fg_bg} + local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg} TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1} local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg} @@ -203,8 +209,8 @@ local function new_view(root, x, y) local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6} for i = 1, 4 do - local tag_fg_bg = cpair(colors.gray, colors.white) - local ind_fg_bg = cpair(colors.lightGray, colors.white) + local tag_fg_bg = gry_wht + local ind_fg_bg = style.lg_white local ind_off = colors.lightGray if i <= facility.num_units then @@ -235,18 +241,18 @@ 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) 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=cpair(colors.gray, colors.white)} + 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) - local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)} + local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=gry_wht} -- save the automatic process control configuration without starting local function _save_cfg() @@ -321,15 +327,15 @@ 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=cpair(colors.black,colors.brown)} - 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=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn} + 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} 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/reactor.lua b/coordinator/ui/components/reactor.lua index b4a2c1b..c74ab0a 100644 --- a/coordinator/ui/components/reactor.lua +++ b/coordinator/ui/components/reactor.lua @@ -14,6 +14,9 @@ local StateIndicator = require("graphics.elements.indicators.state") local cpair = core.cpair local border = core.border +local text_fg_bg = style.text_colors +local lu_col = style.lu_colors + -- create new reactor view ---@param root graphics_element parent ---@param x integer top left x @@ -22,9 +25,6 @@ local border = core.border local function new_view(root, x, y, ps) local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y} - local text_fg_bg = cpair(colors.black, colors.lightGray) - local lu_col = cpair(colors.gray, colors.gray) - local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16} local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} diff --git a/coordinator/ui/components/turbine.lua b/coordinator/ui/components/turbine.lua index 7bd4405..d8b2e17 100644 --- a/coordinator/ui/components/turbine.lua +++ b/coordinator/ui/components/turbine.lua @@ -15,16 +15,16 @@ local VerticalBar = require("graphics.elements.indicators.vbar") local cpair = core.cpair local border = core.border +local text_fg_bg = style.text_colors +local lu_col = style.lu_colors + -- new turbine view ---@param root graphics_element parent ---@param x integer top left x ---@param y integer top left y ---@param ps psil ps interface local function new_view(root, x, y, ps) - local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y} - - local text_fg_bg = cpair(colors.black, colors.lightGray) - local lu_col = cpair(colors.gray, colors.gray) + local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y} local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12} local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg} diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 985e577..64d5e5b 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -2,6 +2,8 @@ -- Reactor Unit SCADA Coordinator GUI -- +local types = require("scada-common.types") + local iocontrol = require("coordinator.iocontrol") local style = require("coordinator.ui.style") @@ -26,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 @@ -34,6 +36,9 @@ local border = core.border local bw_fg_bg = style.bw_fg_bg local lu_cpair = style.lu_colors local hzd_fg_bg = style.hzd_fg_bg +local dis_colors = style.dis_colors + +local gry_wht = style.gray_white local ind_grn = style.ind_grn local ind_yel = style.ind_yel @@ -57,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 -- @@ -93,7 +98,7 @@ local function init(parent, id) waste.register(u_ps, "waste_fill", waste.update) ccool.register(u_ps, "ccool_type", function (type) - if type == "mekanism:sodium" then + if type == types.FLUID.SODIUM then ccool.recolor(cpair(colors.lightBlue, colors.gray)) else ccool.recolor(cpair(colors.blue, colors.gray)) @@ -101,7 +106,7 @@ local function init(parent, id) end) hcool.register(u_ps, "hcool_type", function (type) - if type == "mekanism:superheated_sodium" then + if type == types.FLUID.SUPERHEATED_SODIUM then hcool.recolor(cpair(colors.orange, colors.gray)) else hcool.recolor(cpair(colors.white, colors.gray)) @@ -129,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=cpair(colors.gray, colors.white)} + 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) @@ -190,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} @@ -218,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} @@ -341,10 +346,8 @@ local function init(parent, id) -- reactor controls -- ---------------------- - local dis_colors = cpair(colors.white, colors.lightGray) - - local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)} - local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg} + local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht} + local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg} TextBox{parent=burn_control,x=9,y=2,text="mB/t"} local set_burn = function () unit.set_burn(burn_rate.get_value()) end @@ -379,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} @@ -467,20 +470,20 @@ 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} 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) auto_div.line_break() local function set_group() unit.set_group(group.get_value() - 1) end - local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group} + local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=gry_wht,callback=set_group} auto_div.line_break() diff --git a/coordinator/ui/components/unit_flow.lua b/coordinator/ui/components/unit_flow.lua index 2f371f5..f3103e5 100644 --- a/coordinator/ui/components/unit_flow.lua +++ b/coordinator/ui/components/unit_flow.lua @@ -19,11 +19,10 @@ 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 -local cpair = core.cpair local border = core.border local pipe = core.pipe @@ -31,6 +30,7 @@ local wh_gray = style.wh_gray local bw_fg_bg = style.bw_fg_bg local text_c = style.text_colors local lu_c = style.lu_colors +local lg_gray = style.lg_gray local ind_grn = style.ind_grn local ind_wht = style.ind_wht @@ -64,15 +64,13 @@ local function make(parent, x, y, wide, unit) -- bounding box div local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height} - local lg_gray = cpair(colors.lightGray, colors.gray) - ------------------ -- COOLING LOOP -- ------------------ 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} @@ -111,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} @@ -130,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 @@ -177,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} @@ -205,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 99611a1..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) @@ -261,7 +261,7 @@ local function init(main) if tank_defs[i] > 0 then local vy = 3 + y_ofs(i) - TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=cpair(colors.black,colors.lightGray),width=2,height=1} + TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1} local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn} local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht} @@ -288,8 +288,8 @@ 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=cpair(colors.lightGray,colors.gray)} - TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.wh_gray} + TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_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} @@ -338,8 +338,8 @@ 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=cpair(colors.lightGray,colors.gray)} - TextBox{parent=sps,text="SPS",alignment=TEXT_ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray} + TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_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,8 +361,14 @@ local function init(main) -- statistics -- ---------------- - TextBox{parent=main,x=145,y=16,text="PROC. WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} - local pr_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg} + 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=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} local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17} @@ -371,8 +377,8 @@ 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=23,text="SPENT WASTE",alignment=TEXT_ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} - local sp_waste = Rectangle{parent=main,x=145,y=24,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg} + 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} sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update) diff --git a/coordinator/ui/layout/front_panel.lua b/coordinator/ui/layout/front_panel.lua index 11fe172..ffd2a09 100644 --- a/coordinator/ui/layout/front_panel.lua +++ b/coordinator/ui/layout/front_panel.lua @@ -2,39 +2,41 @@ -- Coordinator Front Panel GUI -- -local types = require("scada-common.types") -local util = require("scada-common.util") +local types = require("scada-common.types") +local util = require("scada-common.util") -local iocontrol = require("coordinator.iocontrol") +local iocontrol = require("coordinator.iocontrol") -local pgi = require("coordinator.ui.pgi") -local style = require("coordinator.ui.style") +local pgi = require("coordinator.ui.pgi") +local style = require("coordinator.ui.style") -local pkt_entry = require("coordinator.ui.components.pkt_entry") +local pkt_entry = require("coordinator.ui.components.pkt_entry") -local core = require("graphics.core") +local core = require("graphics.core") -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 Div = require("graphics.elements.div") +local ListBox = require("graphics.elements.listbox") +local MultiPane = require("graphics.elements.multipane") +local TextBox = require("graphics.elements.textbox") -local TabBar = require("graphics.elements.controls.tabbar") +local TabBar = require("graphics.elements.controls.tabbar") -local LED = require("graphics.elements.indicators.led") -local RGBLED = require("graphics.elements.indicators.ledrgb") +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 +local led_grn = style.led_grn + -- create new front panel view ---@param panel graphics_element main displaybox ---@param num_units integer number of units (number of unit monitors) 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} @@ -47,13 +49,13 @@ local function init(panel, num_units) local system = Div{parent=main_page,width=14,height=17,x=2,y=2} local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} - local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn} status.update(true) system.line_break() heartbeat.register(ps, "heartbeat", heartbeat.update) - local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} + local modem = LED{parent=system,label="MODEM",colors=led_grn} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() @@ -61,25 +63,25 @@ local function init(panel, num_units) modem.register(ps, "has_modem", modem.update) network.register(ps, "link_state", network.update) - local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)} + local speaker = LED{parent=system,label="SPEAKER",colors=led_grn} speaker.register(ps, "has_speaker", speaker.update) ---@diagnostic disable-next-line: undefined-field local comp_id = util.sprintf("(%d)", os.getComputerID()) - TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label} local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2} - local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)} + local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn} main_monitor.register(ps, "main_monitor", main_monitor.update) - local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=cpair(colors.green,colors.green_off)} + local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn} flow_monitor.register(ps, "flow_monitor", flow_monitor.update) monitors.line_break() for i = 1, num_units do - local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)} + local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn} unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update) end @@ -87,9 +89,9 @@ local function init(panel, num_units) -- about footer -- - local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)} - local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} - local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} + 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=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) @@ -101,7 +103,7 @@ local function init(panel, num_units) -- API page local api_page = Div{parent=page_div,x=1,y=1,hidden=true} - local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} + local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp_text,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local _ = Div{parent=api_list,height=1,hidden=true} -- padding -- assemble page panes @@ -111,11 +113,11 @@ local function init(panel, num_units) local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} local tabs = { - { name = "CRD", color = cpair(colors.black, colors.ivory) }, - { name = "API", color = cpair(colors.black, colors.ivory) }, + { name = "CRD", color = style.fp_text }, + { name = "API", color = style.fp_text }, } - TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)} + TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg} -- link pocket API list management to PGI pgi.link_elements(api_list, pkt_entry) diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index a1b3b5b..64742c7 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -2,8 +2,6 @@ -- Main SCADA Coordinator GUI -- -local util = require("scada-common.util") - local iocontrol = require("coordinator.iocontrol") local style = require("coordinator.ui.style") @@ -18,9 +16,7 @@ local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") -local TEXT_ALIGN = core.TEXT_ALIGN - -local cpair = core.cpair +local ALIGN = core.ALIGN -- create new main view ---@param main graphics_element main displaybox @@ -29,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 ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,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) @@ -77,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=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.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/coordinator/ui/style.lua b/coordinator/ui/style.lua index ad23989..d9549a5 100644 --- a/coordinator/ui/style.lua +++ b/coordinator/ui/style.lua @@ -78,11 +78,19 @@ style.lu_colors = cpair(colors.gray, colors.gray) style.hzd_fg_bg = style.wh_gray style.dis_colors = cpair(colors.white, colors.lightGray) +style.lg_gray = cpair(colors.lightGray, colors.gray) +style.lg_white = cpair(colors.lightGray, colors.white) +style.gray_white = cpair(colors.gray, colors.white) + style.ind_grn = cpair(colors.green, colors.gray) style.ind_yel = cpair(colors.yellow, colors.gray) style.ind_red = cpair(colors.red, colors.gray) style.ind_wht = style.wh_gray +style.fp_text = cpair(colors.black, colors.ivory) +style.fp_label = cpair(colors.lightGray, colors.ivory) +style.led_grn = cpair(colors.green, colors.green_off) + -- UI COMPONENTS -- style.reactor = { diff --git a/graphics/core.lua b/graphics/core.lua index 4e56714..7715f92 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,19 +7,15 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "1.1.1" +core.version = "2.0.0" core.flasher = flasher 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 @@ -35,11 +31,7 @@ core.TEXT_ALIGN = { ---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false ---@return graphics_border function core.border(width, color, even) - return { - width = width, - color = color, - even = even or false -- convert nil to false - } + return { width = width, color = color, even = even or false } end ---@class graphics_frame @@ -56,12 +48,7 @@ end ---@param h integer ---@return graphics_frame function core.gframe(x, y, w, h) - return { - x = x, - y = y, - w = w, - h = h - } + return { x = x, y = y, w = w, h = h } end ---@class cpair @@ -82,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 @@ -130,4 +111,215 @@ 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) + return string.sub(msg, (string.find(msg, "@") + 1) or 1) +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 = {} + + -- 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 + else self.censor = nil end + public.show() + end + + -- 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) + + 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) + _write() + elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then + -- write text with cursor at the end, no need to blit + _write() + 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) + + 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 + _write() + 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))) + public.nav_end() + 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() + public.nav_right() + 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 < 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 + 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 + return core diff --git a/graphics/element.lua b/graphics/element.lua index 5bfb7cd..41e4c57 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -2,8 +2,12 @@ -- Generic Graphics Element -- +local util = require("scada-common.util") + local core = require("graphics.core") +local events = core.events + local element = {} ---@class graphics_args_generic @@ -17,6 +21,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 @@ -25,11 +30,14 @@ local element = {} ---|hazard_button_args ---|multi_button_args ---|push_button_args +---|radio_2d_args ---|radio_button_args ---|sidebar_args ---|spinbox_args ---|switch_button_args ---|tabbar_args +---|number_field_args +---|text_field_args ---|alarm_indicator_light ---|core_map_args ---|data_indicator_args @@ -59,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 @@ -67,14 +85,17 @@ 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 - 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 subscriptions = {}, + button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) }, + focused = false, mt = {} } @@ -86,14 +107,13 @@ 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 .. "}: " - -- 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 @@ -101,6 +121,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 -- ------------------------- @@ -134,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) @@ -166,6 +249,31 @@ function element.new(args, child_offset_x, child_offset_y) self.bounds.x2 = self.position.x + f.w - 1 self.bounds.y1 = self.position.y self.bounds.y2 = self.position.y + f.h - 1 + + -- alias functions + + -- window set cursor position + ---@param x integer + ---@param y integer + function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end + + -- set background color + ---@param c color + function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end + + -- set foreground (text) color + ---@param c color + function protected.w_set_fgd(c) protected.window.setTextColor(c) end + + -- write text + ---@param str string + function protected.w_write(str) protected.window.write(str) end + + -- blit text + ---@param str string + ---@param fg string + ---@param bg string + function protected.w_blit(str, fg, bg) protected.window.blit(str, fg, bg) end end -- check if a coordinate relative to the parent is within the bounds of this element @@ -186,85 +294,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 @@ -278,6 +307,107 @@ 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 + + -- focus this element and take away focus from all other elements + function protected.take_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 enabled + function protected.on_enabled() end + + -- handle disabled + function protected.on_disabled() 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 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 + + -- callback on control press responses + ---@param result any + function protected.response_callback(result) end + + -- accessors and control -- + + -- 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 + + -- 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 + + -- re-draw this element + function protected.redraw() end + + -- start animations + function protected.start_anim() end + + -- stop animations + function protected.stop_anim() end + ----------- -- SETUP -- ----------- @@ -289,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 @@ -330,7 +460,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 @@ -352,65 +482,87 @@ 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 + -- 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 ---@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 -- @@ -422,97 +574,114 @@ 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.on_enabled() + end end -- disable the element function public.disable() - protected.enabled = false - protected.disable() + if protected.enabled then + protected.enabled = false + protected.on_disabled() + public.unfocus_all() + 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 protected.enabled 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 -- @@ -525,26 +694,62 @@ 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.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.MOUSE_CLICK.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 + 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 + 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 + + -- 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 + 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() @@ -558,6 +763,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) @@ -567,47 +775,51 @@ 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) - 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 + -- re-draw this element and all its children function public.redraw() - protected.window.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() 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/animations/waiting.lua b/graphics/elements/animations/waiting.lua index 77aacdb..36aa432 100644 --- a/graphics/elements/animations/waiting.lua +++ b/graphics/elements/animations/waiting.lua @@ -36,49 +36,49 @@ local function waiting(args) if state >= 0 and state < 7 then -- top - e.window.setCursorPos(1 + math.floor(state / 2), 1) + e.w_set_cur(1 + math.floor(state / 2), 1) if state % 2 == 0 then - e.window.blit("\x8f", blit_fg, blit_bg) + e.w_blit("\x8f", blit_fg, blit_bg) else - e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x) + e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x) end -- bottom - e.window.setCursorPos(4 - math.ceil(state / 2), 3) + e.w_set_cur(4 - math.ceil(state / 2), 3) if state % 2 == 0 then - e.window.blit("\x8f", blit_fg, blit_bg) + e.w_blit("\x8f", blit_fg, blit_bg) else - e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x) + e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x) end else local st = state - 7 -- right if st % 3 == 0 then - e.window.setCursorPos(4, 1 + math.floor(st / 3)) - e.window.blit("\x83", blit_bg, blit_fg) + e.w_set_cur(4, 1 + math.floor(st / 3)) + e.w_blit("\x83", blit_bg, blit_fg) elseif st % 3 == 1 then - e.window.setCursorPos(4, 1 + math.floor(st / 3)) - e.window.blit("\x8f", blit_bg, blit_fg) - e.window.setCursorPos(4, 2 + math.floor(st / 3)) - e.window.blit("\x83", blit_fg, blit_bg) + e.w_set_cur(4, 1 + math.floor(st / 3)) + e.w_blit("\x8f", blit_bg, blit_fg) + e.w_set_cur(4, 2 + math.floor(st / 3)) + e.w_blit("\x83", blit_fg, blit_bg) else - e.window.setCursorPos(4, 2 + math.floor(st / 3)) - e.window.blit("\x8f", blit_fg, blit_bg) + e.w_set_cur(4, 2 + math.floor(st / 3)) + e.w_blit("\x8f", blit_fg, blit_bg) end -- left if st % 3 == 0 then - e.window.setCursorPos(1, 3 - math.floor(st / 3)) - e.window.blit("\x83", blit_fg, blit_bg) - e.window.setCursorPos(1, 2 - math.floor(st / 3)) - e.window.blit("\x8f", blit_bg, blit_fg) + e.w_set_cur(1, 3 - math.floor(st / 3)) + e.w_blit("\x83", blit_fg, blit_bg) + e.w_set_cur(1, 2 - math.floor(st / 3)) + e.w_blit("\x8f", blit_bg, blit_fg) elseif st % 3 == 1 then - e.window.setCursorPos(1, 2 - math.floor(st / 3)) - e.window.blit("\x83", blit_bg, blit_fg) + e.w_set_cur(1, 2 - math.floor(st / 3)) + e.w_blit("\x83", blit_bg, blit_fg) else - e.window.setCursorPos(1, 2 - math.floor(st / 3)) - e.window.blit("\x8f", blit_fg, blit_bg) + e.w_set_cur(1, 2 - math.floor(st / 3)) + e.w_blit("\x8f", blit_fg, blit_bg) end end diff --git a/graphics/elements/colormap.lua b/graphics/elements/colormap.lua index 4d84e8b..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.window.setCursorPos(1, 1) - e.window.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 226946c..f610393 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 @@ -24,22 +24,17 @@ local CLICK_TYPE = core.events.CLICK_TYPE ---@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") + 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 + args.width = 5 -- create new graphics element base object local e = element.new(args) - -- write app title, centered - e.window.setCursorPos(1, 4) - e.window.setCursorPos(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4) - e.window.write(args.title) - -- draw the app button local function draw() local fgd = args.app_fg_bg.fgd @@ -51,36 +46,36 @@ local function app_button(args) end -- draw icon - e.window.setCursorPos(1, 1) - e.window.setTextColor(fgd) - e.window.setBackgroundColor(bkg) - e.window.write("\x9f\x83\x83\x83") - e.window.setTextColor(bkg) - e.window.setBackgroundColor(fgd) - e.window.write("\x90") - e.window.setTextColor(fgd) - e.window.setBackgroundColor(bkg) - e.window.setCursorPos(1, 2) - e.window.write("\x95 ") - e.window.setTextColor(bkg) - e.window.setBackgroundColor(fgd) - e.window.write("\x95") - e.window.setCursorPos(1, 3) - e.window.write("\x82\x8f\x8f\x8f\x81") + e.w_set_cur(1, 1) + e.w_set_fgd(fgd) + e.w_set_bkg(bkg) + e.w_write("\x9f\x83\x83\x83") + e.w_set_fgd(bkg) + e.w_set_bkg(fgd) + e.w_write("\x90") + e.w_set_fgd(fgd) + e.w_set_bkg(bkg) + e.w_set_cur(1, 2) + e.w_write("\x95 ") + e.w_set_fgd(bkg) + e.w_set_bkg(fgd) + e.w_write("\x95") + e.w_set_cur(1, 3) + e.w_write("\x82\x8f\x8f\x8f\x81") -- write the icon text - e.window.setCursorPos(3, 2) - e.window.setTextColor(fgd) - e.window.setBackgroundColor(bkg) - e.window.write(args.text) + e.w_set_cur(3, 2) + e.w_set_fgd(fgd) + e.w_set_bkg(bkg) + e.w_write(args.text) end -- draw the app button as pressed (if active_fg_bg set) local function show_pressed() if e.enabled and args.active_fg_bg ~= nil then e.value = true - e.window.setTextColor(args.active_fg_bg.fgd) - e.window.setBackgroundColor(args.active_fg_bg.bkg) + e.w_set_fgd(args.active_fg_bg.fgd) + e.w_set_bkg(args.active_fg_bg.bkg) draw() end end @@ -89,8 +84,8 @@ local function app_button(args) local function show_unpressed() if e.enabled and args.active_fg_bg ~= nil then e.value = false - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) draw() end end @@ -99,14 +94,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() @@ -118,11 +113,18 @@ 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 + + -- element redraw + function e.redraw() + e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4) + e.w_write(args.title) + 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 b8557ae..08e8bc5 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -6,7 +6,8 @@ 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 default? boolean default value +---@field callback? function function to call on press ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -18,48 +19,75 @@ 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.callback) == "function", "graphics.elements.controls.checkbox: callback 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 args.width = 3 + string.len(args.label) -- 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() - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if e.value then -- show as selected - e.window.setTextColor(args.box_fg_bg.bkg) - e.window.setBackgroundColor(args.box_fg_bg.fgd) - e.window.write("\x88") - e.window.setTextColor(args.box_fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) - e.window.write("\x95") + e.w_set_fgd(args.box_fg_bg.bkg) + e.w_set_bkg(args.box_fg_bg.fgd) + e.w_write("\x88") + e.w_set_fgd(args.box_fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + e.w_write("\x95") else -- show as unselected - e.window.setTextColor(e.fg_bg.bkg) - e.window.setBackgroundColor(args.box_fg_bg.bkg) - e.window.write("\x88") - e.window.setTextColor(args.box_fg_bg.bkg) - e.window.setBackgroundColor(e.fg_bg.bkg) - e.window.write("\x95") + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(args.box_fg_bg.bkg) + e.w_write("\x88") + e.w_set_fgd(args.box_fg_bg.bkg) + e.w_set_bkg(e.fg_bg.bkg) + e.w_write("\x95") + 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) - 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 + + -- 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() + if type(args.callback) == "function" then args.callback(e.value) end + end end end @@ -70,14 +98,22 @@ local function checkbox(args) draw() end - -- write label text - e.window.setCursorPos(3, 1) - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) - e.window.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 + + -- element redraw + function e.redraw() + draw() + draw_label() + end -- initial draw - draw() + e.redraw() return e.complete() end diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua index fcec4bd..9a0e621 100644 --- a/graphics/elements/controls/hazard_button.lua +++ b/graphics/elements/controls/hazard_button.lua @@ -1,7 +1,6 @@ -- Hazard-bordered Button Graphics Element local tcd = require("scada-common.tcd") -local util = require("scada-common.util") local core = require("graphics.core") local element = require("graphics.element") @@ -22,47 +21,42 @@ 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") + 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") - -- 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.window.setCursorPos(3, 2) - e.window.write(args.text) - -- draw border ---@param accent color accent color local function draw_border(accent) -- top - e.window.setTextColor(accent) - e.window.setBackgroundColor(args.fg_bg.bkg) - e.window.setCursorPos(1, 1) - e.window.write("\x99" .. util.strrep("\x89", args.width - 2) .. "\x99") + e.w_set_fgd(accent) + e.w_set_bkg(args.fg_bg.bkg) + e.w_set_cur(1, 1) + e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99") -- center left - e.window.setCursorPos(1, 2) - e.window.setTextColor(args.fg_bg.bkg) - e.window.setBackgroundColor(accent) - e.window.write("\x99") + e.w_set_cur(1, 2) + e.w_set_fgd(args.fg_bg.bkg) + e.w_set_bkg(accent) + e.w_write("\x99") -- center right - e.window.setTextColor(args.fg_bg.bkg) - e.window.setBackgroundColor(accent) - e.window.setCursorPos(args.width, 2) - e.window.write("\x99") + e.w_set_fgd(args.fg_bg.bkg) + e.w_set_bkg(accent) + e.w_set_cur(args.width, 2) + e.w_write("\x99") -- bottom - e.window.setTextColor(accent) - e.window.setBackgroundColor(args.fg_bg.bkg) - e.window.setCursorPos(1, 3) - e.window.write("\x99" .. util.strrep("\x98", args.width - 2) .. "\x99") + e.w_set_fgd(accent) + e.w_set_bkg(args.fg_bg.bkg) + e.w_set_cur(1, 3) + e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99") end -- on request timeout: recursively calls itself to double flash button text @@ -73,9 +67,9 @@ local function hazard_button(args) if n == 0 then -- go back off - e.window.setTextColor(args.fg_bg.fgd) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.fg_bg.fgd) + e.w_set_cur(3, 2) + e.w_write(args.text) end if n >= 4 then @@ -83,18 +77,18 @@ local function hazard_button(args) elseif n % 2 == 0 then -- toggle text color on after 0.25 seconds tcd.dispatch(0.25, function () - e.window.setTextColor(args.accent) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.accent) + e.w_set_cur(3, 2) + e.w_write(args.text) on_timeout(n + 1) on_timeout(n + 1) end) elseif n % 1 then -- toggle text color off after 0.25 seconds tcd.dispatch(0.25, function () - e.window.setTextColor(args.fg_bg.fgd) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.fg_bg.fgd) + e.w_set_cur(3, 2) + e.w_write(args.text) on_timeout(n + 1) end) end @@ -102,9 +96,9 @@ local function hazard_button(args) -- blink routine for success indication local function on_success() - e.window.setTextColor(args.fg_bg.fgd) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.fg_bg.fgd) + e.w_set_cur(3, 2) + e.w_write(args.text) end -- blink routine for failure indication @@ -115,9 +109,9 @@ local function hazard_button(args) if n == 0 then -- go back off - e.window.setTextColor(args.fg_bg.fgd) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.fg_bg.fgd) + e.w_set_cur(3, 2) + e.w_write(args.text) end if n >= 2 then @@ -125,17 +119,17 @@ local function hazard_button(args) elseif n % 2 == 0 then -- toggle text color on after 0.5 seconds tcd.dispatch(0.5, function () - e.window.setTextColor(args.accent) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.accent) + e.w_set_cur(3, 2) + e.w_write(args.text) on_failure(n + 1) end) elseif n % 1 then -- toggle text color off after 0.25 seconds tcd.dispatch(0.25, function () - e.window.setTextColor(args.fg_bg.fgd) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.fg_bg.fgd) + e.w_set_cur(3, 2) + e.w_write(args.text) on_failure(n + 1) end) end @@ -147,9 +141,9 @@ local function hazard_button(args) if e.enabled then if core.events.was_clicked(event.type) then -- change text color to indicate clicked - e.window.setTextColor(args.accent) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.accent) + e.w_set_cur(3, 2) + e.w_write(args.text) -- abort any other callbacks tcd.abort(on_timeout) @@ -159,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 @@ -175,29 +168,37 @@ 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 - function e.disable() + function e.on_disabled() if args.dis_colors then draw_border(args.dis_colors.color_a) - e.window.setTextColor(args.dis_colors.color_b) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.dis_colors.color_b) + e.w_set_cur(3, 2) + e.w_write(args.text) end end -- show the button as enabled - function e.enable() + function e.on_enabled() draw_border(args.accent) - e.window.setTextColor(args.fg_bg.fgd) - e.window.setCursorPos(3, 2) - e.window.write(args.text) + e.w_set_fgd(args.fg_bg.fgd) + e.w_set_cur(3, 2) + 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 020c68d..d686b9d 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") + 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 @@ -71,23 +69,23 @@ 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 - e.window.setCursorPos(opt._start_x, 1) + e.w_set_cur(opt._start_x, 1) if e.value == i then -- show as pressed - e.window.setTextColor(opt.active_fg_bg.fgd) - e.window.setBackgroundColor(opt.active_fg_bg.bkg) + e.w_set_fgd(opt.active_fg_bg.fgd) + e.w_set_bkg(opt.active_fg_bg.bkg) else -- show as unpressed - e.window.setTextColor(opt.fg_bg.fgd) - e.window.setBackgroundColor(opt.fg_bg.bkg) + e.w_set_fgd(opt.fg_bg.fgd) + e.w_set_bkg(opt.fg_bg.bkg) end - e.window.write(util.pad(opt.text, button_width)) + e.w_write(util.pad(opt.text, button_width)) end end @@ -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 5644e59..59e2b9e 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 @@ -25,14 +26,14 @@ local CLICK_TYPE = core.events.CLICK_TYPE ---@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") + 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) - -- 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) @@ -44,21 +45,21 @@ 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 - e.window.setCursorPos(h_pad, v_pad) - e.window.write(args.text) + e.w_set_cur(h_pad, v_pad) + e.w_write(args.text) end -- draw the button as pressed (if active_fg_bg set) local function show_pressed() if e.enabled and args.active_fg_bg ~= nil then e.value = true - e.window.setTextColor(args.active_fg_bg.fgd) - e.window.setBackgroundColor(args.active_fg_bg.bkg) - draw() + e.w_set_fgd(args.active_fg_bg.fgd) + e.w_set_bkg(args.active_fg_bg.bkg) + e.redraw() end end @@ -66,9 +67,9 @@ local function push_button(args) local function show_unpressed() if e.enabled and args.active_fg_bg ~= nil then e.value = false - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) - draw() + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + e.redraw() end end @@ -76,14 +77,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,34 +93,49 @@ 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 - function e.enable() + function e.on_enabled() if args.dis_fg_bg ~= nil then e.value = false - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) - draw() + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + e.redraw() end end -- show button as disabled - function e.disable() + function e.on_disabled() if args.dis_fg_bg ~= nil then e.value = false - e.window.setTextColor(args.dis_fg_bg.fgd) - e.window.setBackgroundColor(args.dis_fg_bg.bkg) - draw() + e.w_set_fgd(args.dis_fg_bg.fgd) + e.w_set_bkg(args.dis_fg_bg.bkg) + e.redraw() end end + -- handle focus + e.on_focused = show_pressed + 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 new file mode 100644 index 0000000..65d4c09 --- /dev/null +++ b/graphics/elements/controls/radio_2d.lua @@ -0,0 +1,203 @@ +-- 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) + 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 = {} + + 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 + + -- 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) + + 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 + e.redraw() + 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 + 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 + e.redraw() + end + elseif event.key == keys.up then + if focused_opt > 1 then + focused_opt = focused_opt - 1 + 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 + 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 + e.redraw() + elseif array[#array][focus_x] then + focused_opt = array[#array][focus_x].id + e.redraw() + end + end + end + end + + -- set the value + ---@param val integer new value + function e.set_value(val) + if type(val) == "number" and val > 0 and val <= #args.options then + e.value = val + e.redraw() + end + end + + -- handle focus & enable + e.on_focused = e.redraw + e.on_unfocused = e.redraw + e.on_enabled = e.redraw + e.on_disabled = e.redraw + + -- initial draw + e.redraw() + + 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 0063bee..aee7060 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -1,15 +1,19 @@ -- Radio Button 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 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 callback? function function to call on touch ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -21,16 +25,12 @@ local element = require("graphics.element") ---@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.callback) == "function", "graphics.elements.controls.radio_button: callback 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), - "graphics.elements.controls.radio_button: min_width must be nil or a number > 0") - - -- one line per option - args.height = #args.options + 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 @@ -43,41 +43,47 @@ 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 -- show the button state - local function draw() + function e.redraw() for i = 1, #args.options do local opt = args.options[i] ---@type string - e.window.setCursorPos(1, i) + 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) - if e.value == i then - -- show as selected - e.window.setTextColor(args.radio_colors.color_a) - e.window.setBackgroundColor(args.radio_bg) - else - -- show as unselected - e.window.setTextColor(args.radio_colors.color_b) - e.window.setBackgroundColor(args.radio_bg) - end + e.w_set_cur(1, i) - e.window.write("\x88") + e.w_set_fgd(inner_color) + e.w_set_bkg(outer_color) + e.w_write("\x88") - e.window.setTextColor(args.radio_bg) - e.window.setBackgroundColor(e.fg_bg.bkg) - e.window.write("\x95") + e.w_set_fgd(outer_color) + e.w_set_bkg(e.fg_bg.bkg) + e.w_write("\x95") -- write button text - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) - e.window.write(opt) + 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 @@ -88,8 +94,31 @@ local function radio_button(args) -- determine what was pressed if args.options[event.current.y] ~= nil then e.value = event.current.y - draw() - args.callback(e.value) + focused_opt = e.value + e.redraw() + if type(args.callback) == "function" then args.callback(e.value) end + end + end + end + + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + 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 + 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 + e.redraw() + end + elseif event.key == keys.up then + if focused_opt > 1 then + focused_opt = focused_opt - 1 + e.redraw() + end end end end @@ -97,12 +126,20 @@ local function radio_button(args) -- set the value ---@param val integer new value function e.set_value(val) - e.value = val - draw() + if type(val) == "number" and val > 0 and val <= #args.options then + e.value = val + e.redraw() + end end + -- handle focus & enable + e.on_focused = e.redraw + e.on_unfocused = e.redraw + 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 977481c..fef8a8a 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/sidebar.lua @@ -1,11 +1,12 @@ -- 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") -local CLICK_TYPE = core.events.CLICK_TYPE +local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class sidebar_tab ---@field char string character identifier @@ -26,25 +27,28 @@ local CLICK_TYPE = core.events.CLICK_TYPE ---@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") + 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") - -- always 3 wide 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 + 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 @@ -52,27 +56,23 @@ local function sidebar(args) local y = ((i - 1) * 3) + 1 - e.window.setCursorPos(1, y) + e.w_set_cur(1, y) if pressed and i == pressed_idx then - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) else - e.window.setTextColor(tab.color.fgd) - e.window.setBackgroundColor(tab.color.bkg) + e.w_set_fgd(tab.color.fgd) + e.w_set_bkg(tab.color.bkg) end - e.window.write(" ") - e.window.setCursorPos(1, y + 1) + e.w_write(" ") + e.w_set_cur(1, y + 1) if e.value == i then - -- show as selected - e.window.write(" " .. tab.char .. "\x10") - else - -- show as unselected - e.window.write(" " .. tab.char .. " ") - end - e.window.setCursorPos(1, y + 2) - e.window.write(" ") + e.w_write(" " .. tab.char .. "\x10") + else e.w_write(" " .. tab.char .. " ") end + e.w_set_cur(1, y + 2) + e.w_write(" ") end end @@ -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 @@ -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 1cdfa26..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), "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") + 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", "graphics.element.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) @@ -58,22 +58,20 @@ local function spinbox(args) -- draw the arrows local function draw_arrows(color) - e.window.setBackgroundColor(args.arrow_fg_bg.bkg) - e.window.setTextColor(color) - e.window.setCursorPos(1, 1) - e.window.write(util.strrep("\x1e", wn_prec)) - e.window.setCursorPos(1, 3) - e.window.write(util.strrep("\x1f", wn_prec)) + e.w_set_bkg(args.arrow_fg_bg.bkg) + e.w_set_fgd(color) + e.w_set_cur(1, 1) + e.w_write(string.rep("\x1e", wn_prec)) + e.w_set_cur(1, 3) + e.w_write(string.rep("\x1f", wn_prec)) if fr_prec > 0 then - e.window.setCursorPos(1 + wn_prec, 1) - e.window.write(" " .. util.strrep("\x1e", fr_prec)) - e.window.setCursorPos(1 + wn_prec, 3) - e.window.write(" " .. util.strrep("\x1f", fr_prec)) + e.w_set_cur(1 + wn_prec, 1) + e.w_write(" " .. string.rep("\x1e", fr_prec)) + e.w_set_cur(1 + wn_prec, 3) + e.w_write(" " .. string.rep("\x1f", fr_prec)) 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) @@ -119,15 +117,12 @@ local function spinbox(args) end -- draw - e.window.setBackgroundColor(e.fg_bg.bkg) - e.window.setTextColor(e.fg_bg.fgd) - e.window.setCursorPos(1, 2) - e.window.write(util.sprintf(fmt, e.value)) + e.w_set_bkg(e.fg_bg.bkg) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_cur(1, 2) + 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.enable() - 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.disable() - 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 92fd9a5..f649b10 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") + 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) - -- 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.window.setTextColor(args.active_fg_bg.fgd) - e.window.setBackgroundColor(args.active_fg_bg.bkg) + e.w_set_fgd(args.active_fg_bg.fgd) + e.w_set_bkg(args.active_fg_bg.bkg) else - -- show as unpressed - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) + 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.window.setCursorPos(h_pad, v_pad) - e.window.write(args.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 0989534..9b604e1 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") + 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") - -- always 1 tall args.height = 1 -- determine widths @@ -50,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 @@ -67,21 +65,21 @@ 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 - e.window.setCursorPos(tab._start_x, 1) + e.w_set_cur(tab._start_x, 1) if e.value == i then - e.window.setTextColor(tab.color.fgd) - e.window.setBackgroundColor(tab.color.bkg) + e.w_set_fgd(tab.color.fgd) + e.w_set_bkg(tab.color.bkg) else - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) end - e.window.write(util.pad(tab.name, button_width)) + e.w_write(util.pad(tab.name, button_width)) end end @@ -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 new file mode 100644 index 0000000..53fc473 --- /dev/null +++ b/graphics/elements/form/number_field.lua @@ -0,0 +1,156 @@ +-- Numeric Value Entry Graphics Element + +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 +---@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 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 numeric entry 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) + + -- make an interactive field manager + local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg) + + -- 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 then + if core.events.was_clicked(event.type) then + e.take_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 + + -- 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 + if e.value == 0 then e.value = "" end + ifield.try_insert_char(event.name) + end + 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 + elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then + has_decimal = true + 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 + 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() + 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 + + -- 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 + end + + -- set minimum input value + ---@param min integer minimum allowed value + 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 + e.on_unfocused() + 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 unfocused + 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 + ifield.nav_start() + elseif type(args.min) == "number" and val < min then + e.value = "" .. min + ifield.nav_start() + end + else + e.value = "" + end + + ifield.show() + end + + -- 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 + e.redraw() + + return e.complete() +end + +return number_field diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua new file mode 100644 index 0000000..aee910d --- /dev/null +++ b/graphics/elements/form/text_field.lua @@ -0,0 +1,105 @@ +-- Text Value Entry Graphics Element + +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 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 +---@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, function censor_ctl +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 "" + + -- 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) + -- only handle if on an increment or decrement arrow + if e.enabled then + if core.events.was_clicked(event.type) then + e.take_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 + + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + 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 + ifield.backspace() + 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() + 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 + + -- set the value + ---@param val string string to set + function e.set_value(val) + ifield.set_value(val) + end + + -- replace text with pasted text + ---@param text string string to set + function e.handle_paste(text) + ifield.set_value(text) + end + + -- handle focus, enable, and redraw with show() + e.on_focused = ifield.show + e.on_unfocused = ifield.show + e.on_enabled = ifield.show + e.on_disabled = ifield.show + e.redraw = ifield.show + + -- initial draw + e.redraw() + + local elem, id = e.complete() + return elem, id, ifield.censor +end + +return text_field diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua index b3e9ab8..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", "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") + 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), "graphics.elements.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 @@ -51,19 +51,21 @@ 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.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if flash_on then if e.value == 2 then - e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) end else if e.value == 3 then - e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) else - e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) end end @@ -76,7 +78,7 @@ local function alarm_indicator_light(args) local was_off = e.value ~= 2 e.value = new_state - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if args.flash then if was_off and (new_state == 2) then @@ -87,17 +89,17 @@ local function alarm_indicator_light(args) flasher.stop(flash_callback) if new_state == 3 then - e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) else - e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) end end elseif new_state == 2 then - e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) elseif new_state == 3 then - e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) else - e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) end end @@ -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.window.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 997bec8..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), "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") + 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 @@ -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 @@ -47,25 +49,25 @@ local function core_map(args) -- create coordinate grid and frame local function draw_frame() - e.window.setTextColor(colors.white) + e.w_set_fgd(colors.white) for x = 0, (inner_width - 1) do - e.window.setCursorPos(x + start_x, 1) - e.window.write(util.sprintf("%X", x)) + e.w_set_cur(x + start_x, 1) + e.w_write(util.sprintf("%X", x)) end for y = 0, (inner_height - 1) do - e.window.setCursorPos(1, y + start_y) - e.window.write(util.sprintf("%X", y)) + e.w_set_cur(1, y + start_y) + e.w_write(util.sprintf("%X", y)) end -- even out bottom edge - e.window.setTextColor(e.fg_bg.bkg) - e.window.setBackgroundColor(args.parent.get_fg_bg().bkg) - e.window.setCursorPos(1, e.frame.h) - e.window.write(util.strrep("\x8f", e.frame.w)) - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(args.parent.get_fg_bg().bkg) + e.w_set_cur(1, e.frame.h) + e.w_write(string.rep("\x8f", e.frame.w)) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) end -- draw the core @@ -102,13 +104,13 @@ local function core_map(args) -- draw pattern for y = start_y, inner_height + (start_y - 1) do - e.window.setCursorPos(start_x, y) + e.w_set_cur(start_x, y) for _ = 1, inner_width do if alternator then i = i + 1 - e.window.blit("\x07", text_c, back_c) + e.w_blit("\x07", text_c, back_c) else - e.window.blit("\x07", "7", "8") + e.w_blit("\x07", "7", "8") end alternator = not alternator @@ -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 5717d2c..2304807 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") + 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") - -- 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.window.setTextColor(args.lu_colors.color_a) - end - - -- write label - e.window.setCursorPos(1, 1) - e.window.write(args.label) + e.value = args.value local value_color = e.fg_bg.fgd local label_len = string.len(args.label) @@ -60,25 +52,25 @@ local function data(args) e.value = value -- clear old data and label - e.window.setCursorPos(data_start, 1) - e.window.write(util.spaces(clear_width)) + e.w_set_cur(data_start, 1) + e.w_write(util.spaces(clear_width)) -- write data local data_str = util.sprintf(args.format, value) - e.window.setCursorPos(data_start, 1) - e.window.setTextColor(value_color) + e.w_set_cur(data_start, 1) + e.w_set_fgd(value_color) if args.commas then - e.window.write(util.comma_format(data_str)) + e.w_write(util.comma_format(data_str)) else - e.window.write(data_str) + e.w_write(data_str) end -- write label if args.unit ~= nil then if args.lu_colors ~= nil then - e.window.setTextColor(args.lu_colors.color_b) + e.w_set_fgd(args.lu_colors.color_b) end - e.window.write(" " .. args.unit) + e.w_write(" " .. args.unit) end end @@ -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 0d61374..eb4607a 100644 --- a/graphics/elements/indicators/hbar.lua +++ b/graphics/elements/indicators/hbar.lua @@ -22,16 +22,17 @@ 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) + 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") + 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 @@ -87,16 +88,16 @@ local function hbar(args) -- draw bar for y = 1, e.frame.h do - e.window.setCursorPos(1, y) + e.w_set_cur(1, y) -- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite - e.window.blit(spaces, bkg, fgd) + e.w_blit(spaces, bkg, fgd) end end -- update percentage if args.show_percent then - e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2))) - e.window.write(util.sprintf("%3.0f%%", fraction * 100)) + e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2))) + e.w_write(util.sprintf("%3.0f%%", fraction * 100)) end end @@ -105,20 +106,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 710fa5b..5c2fc4e 100644 --- a/graphics/elements/indicators/icon.lua +++ b/graphics/elements/indicators/icon.lua @@ -1,7 +1,5 @@ -- Icon Indicator Graphics Element -local util = require("scada-common.util") - local element = require("graphics.element") ---@class icon_sym_color @@ -25,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") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.states) == "table", "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 @@ -44,30 +41,34 @@ local function icon(args) table.insert(state_blit_cmds, { text = " " .. sym_color.symbol .. " ", - fgd = util.strrep(sym_color.color.blit_fgd, 3), - bkg = util.strrep(sym_color.color.blit_bkg, 3) + fgd = string.rep(sym_color.color.blit_fgd, 3), + bkg = string.rep(sym_color.color.blit_bkg, 3) }) end - -- write label and initial indicator light - e.window.setCursorPos(5, 1) - e.window.write(args.label) - -- 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.window.setCursorPos(1, 1) - e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) + e.w_set_cur(1, 1) + e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) end -- set indicator state ---@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 f81d7e9..011ee62 100644 --- a/graphics/elements/indicators/led.lua +++ b/graphics/elements/indicators/led.lua @@ -23,33 +23,31 @@ 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") + 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), "graphics.elements.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 - -- 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.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if flash_on then - e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) + e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) else - e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) + e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) end flash_on = not flash_on @@ -61,8 +59,8 @@ local function indicator_led(args) flash_on = true flasher.start(flash_callback, args.period) else - e.window.setCursorPos(1, 1) - e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) + e.w_set_cur(1, 1) + e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg) end end @@ -73,8 +71,8 @@ local function indicator_led(args) flasher.stop(flash_callback) end - e.window.setCursorPos(1, 1) - e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) + e.w_set_cur(1, 1) + e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg) end -- on state change @@ -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.window.setCursorPos(3, 1) - e.window.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 97099d2..1a81854 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") + 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), "graphics.elements.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 - -- 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,21 +46,20 @@ 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 local function flash_callback() - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if flash_on then if e.value == 2 then - e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) + e.w_blit("\x8c", c1, e.fg_bg.blit_bkg) elseif e.value == 3 then - e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) + e.w_blit("\x8c", c2, e.fg_bg.blit_bkg) end else - e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + e.w_blit("\x8c", co, e.fg_bg.blit_bkg) end flash_on = not flash_on @@ -77,7 +71,7 @@ local function indicator_led_pair(args) local was_off = e.value <= 1 e.value = new_state - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if args.flash then if was_off and (new_state > 1) then @@ -86,15 +80,14 @@ local function indicator_led_pair(args) elseif new_state <= 1 then flash_on = false flasher.stop(flash_callback) - - e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + e.w_blit("\x8c", co, e.fg_bg.blit_bkg) end elseif new_state == 2 then - e.window.blit("\x8c", c1, e.fg_bg.blit_bkg) + e.w_blit("\x8c", c1, e.fg_bg.blit_bkg) elseif new_state == 3 then - e.window.blit("\x8c", c2, e.fg_bg.blit_bkg) + e.w_blit("\x8c", c2, e.fg_bg.blit_bkg) else - e.window.blit("\x8c", co, e.fg_bg.blit_bkg) + e.w_blit("\x8c", co, e.fg_bg.blit_bkg) end end @@ -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.window.setCursorPos(3, 1) - e.window.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 8c9802f..406e0cc 100644 --- a/graphics/elements/indicators/ledrgb.lua +++ b/graphics/elements/indicators/ledrgb.lua @@ -18,28 +18,24 @@ 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") + element.assert(type(args.label) == "string", "label is a required field") + element.assert(type(args.colors) == "table", "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 ---@param new_state integer indicator state function e.on_update(new_state) e.value = new_state - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if type(args.colors[new_state]) == "number" then - e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg) + e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg) end end @@ -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.window.setCursorPos(3, 1) - e.window.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 2f68e9b..290118c 100644 --- a/graphics/elements/indicators/light.lua +++ b/graphics/elements/indicators/light.lua @@ -23,33 +23,31 @@ 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") + 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), "graphics.elements.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 - -- 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.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if flash_on then - e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) else - e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) end flash_on = not flash_on @@ -61,8 +59,8 @@ local function indicator_light(args) flash_on = true flasher.start(flash_callback, args.period) else - e.window.setCursorPos(1, 1) - e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) + e.w_set_cur(1, 1) + e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg) end end @@ -73,8 +71,8 @@ local function indicator_light(args) flasher.stop(flash_callback) end - e.window.setCursorPos(1, 1) - e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) + e.w_set_cur(1, 1) + e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg) end -- on state change @@ -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.window.setCursorPos(3, 1) - e.window.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 5f1641e..69e4a0b 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") + element.assert(type(args.value) == "number", "value is a required field") + element.assert(util.is_int(args.width), "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.window.setTextColor(args.lu_colors.color_a) - end + e.value = args.value - -- write label - e.window.setCursorPos(1, 1) - e.window.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 @@ -52,13 +43,13 @@ local function power(args) local data_str, unit = util.power_format(value, false, args.format) -- write data - e.window.setCursorPos(data_start, 1) - e.window.setTextColor(e.fg_bg.fgd) - e.window.write(util.comma_format(data_str)) + e.w_set_cur(data_start, 1) + e.w_set_fgd(e.fg_bg.fgd) + e.w_write(util.comma_format(data_str)) -- write unit if args.lu_colors ~= nil then - e.window.setTextColor(args.lu_colors.color_b) + e.w_set_fgd(args.lu_colors.color_b) end -- append per tick if rate is set @@ -70,15 +61,27 @@ local function power(args) if unit == "FE" then unit = "FE " end end - e.window.write(" " .. unit) + e.w_write(" " .. unit) end -- set the value ---@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 83c9619..545ea41 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? radiation_reading default value ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -24,24 +24,16 @@ 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") + 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") - -- 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.window.setTextColor(args.lu_colors.color_a) - end - - -- write label - e.window.setCursorPos(1, 1) - e.window.write(args.label) + e.value = args.value or types.new_zero_radiation_reading() local label_len = string.len(args.label) local data_start = 1 @@ -58,32 +50,41 @@ local function rad(args) e.value = value.radiation -- clear old data and label - e.window.setCursorPos(data_start, 1) - e.window.write(util.spaces(clear_width)) + e.w_set_cur(data_start, 1) + e.w_write(util.spaces(clear_width)) -- write data local data_str = util.sprintf(args.format, e.value) - e.window.setCursorPos(data_start, 1) - e.window.setTextColor(e.fg_bg.fgd) + e.w_set_cur(data_start, 1) + e.w_set_fgd(e.fg_bg.fgd) if args.commas then - e.window.write(util.comma_format(data_str)) + e.w_write(util.comma_format(data_str)) else - e.window.write(data_str) + e.w_write(data_str) end -- write unit if args.lu_colors ~= nil then - e.window.setTextColor(args.lu_colors.color_b) + e.w_set_fgd(args.lu_colors.color_b) end - e.window.write(" " .. value.unit) + e.w_write(" " .. value.unit) end -- set the value ---@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 cff1798..f2dc134 100644 --- a/graphics/elements/indicators/state.lua +++ b/graphics/elements/indicators/state.lua @@ -25,16 +25,12 @@ 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") + element.assert(type(args.states) == "table", "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") - else - args.height = 1 - end + element.assert(args.height % 2 == 1, "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 +38,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 @@ -51,21 +46,28 @@ local function state_indicator(args) table.insert(state_blit_cmds, { text = text, - fgd = util.strrep(state_def.color.blit_fgd, string.len(text)), - bkg = util.strrep(state_def.color.blit_bkg, string.len(text)) + fgd = string.rep(state_def.color.blit_fgd, string.len(text)), + bkg = string.rep(state_def.color.blit_bkg, string.len(text)) }) end -- 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.window.setCursorPos(1, 1) - e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg) + e.redraw() end -- set indicator state @@ -73,7 +75,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 151463f..f5d441c 100644 --- a/graphics/elements/indicators/trilight.lua +++ b/graphics/elements/indicators/trilight.lua @@ -25,47 +25,41 @@ 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") + 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), "graphics.elements.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 - -- 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.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if flash_on then if e.value == 2 then - e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) elseif e.value == 3 then - e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) end else - e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) end flash_on = not flash_on @@ -77,7 +71,7 @@ local function tristate_indicator_light(args) local was_off = e.value <= 1 e.value = new_state - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) if args.flash then if was_off and (new_state > 1) then @@ -87,14 +81,14 @@ local function tristate_indicator_light(args) flash_on = false flasher.stop(flash_callback) - e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) end elseif new_state == 2 then - e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg) elseif new_state == 3 then - e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg) else - e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) + e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg) end end @@ -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.window.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 36cc74d..afe56fc 100644 --- a/graphics/elements/indicators/vbar.lua +++ b/graphics/elements/indicators/vbar.lua @@ -20,18 +20,18 @@ 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 - local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w) - local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w) + 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) - local one_third = util.strrep("\x8f", e.frame.w) - local two_thirds = util.strrep("\x83", e.frame.w) + local one_third = string.rep("\x8f", e.frame.w) + local two_thirds = string.rep("\x83", e.frame.w) -- handle data changes ---@param fraction number 0.0 to 1.0 @@ -52,54 +52,55 @@ 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.window.setCursorPos(1, y) + e.w_set_cur(1, y) -- fill percentage for _ = 1, num_bars / 3 do - e.window.blit(spaces, bkg, fgd) + e.w_blit(spaces, bkg, fgd) y = y - 1 - e.window.setCursorPos(1, y) + e.w_set_cur(1, y) end -- add fractional bar if needed if num_bars % 3 == 1 then - e.window.blit(one_third, bkg, fgd) + e.w_blit(one_third, bkg, fgd) y = y - 1 elseif num_bars % 3 == 2 then - e.window.blit(two_thirds, bkg, fgd) + e.w_blit(two_thirds, bkg, fgd) y = y - 1 end -- fill the rest blank while y > 0 do - e.window.setCursorPos(1, y) - e.window.blit(spaces, fgd, bkg) + e.w_set_cur(1, y) + e.w_blit(spaces, fgd, bkg) y = y - 1 end end end - -- change bar color - ---@param fg_bg cpair new bar colors - function e.recolor(fg_bg) - fgd = util.strrep(fg_bg.blit_fgd, e.frame.w) - bkg = util.strrep(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 - 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) + e.redraw() + end + + -- initial draw + e.redraw() + return e.complete() end diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index 7be8e77..d138e19 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) @@ -63,34 +63,34 @@ local function listbox(args) -- draw up/down arrows if pressed_arrow == 1 then - e.window.setTextColor(active_fg_bg.fgd) - e.window.setBackgroundColor(active_fg_bg.bkg) - e.window.setCursorPos(e.frame.w, 1) - e.window.write("\x1e") - e.window.setTextColor(nav_fg_bg.fgd) - e.window.setBackgroundColor(nav_fg_bg.bkg) - e.window.setCursorPos(e.frame.w, e.frame.h) - e.window.write("\x1f") + e.w_set_fgd(active_fg_bg.fgd) + e.w_set_bkg(active_fg_bg.bkg) + e.w_set_cur(e.frame.w, 1) + e.w_write("\x1e") + e.w_set_fgd(nav_fg_bg.fgd) + e.w_set_bkg(nav_fg_bg.bkg) + e.w_set_cur(e.frame.w, e.frame.h) + e.w_write("\x1f") elseif pressed_arrow == -1 then - e.window.setTextColor(nav_fg_bg.fgd) - e.window.setBackgroundColor(nav_fg_bg.bkg) - e.window.setCursorPos(e.frame.w, 1) - e.window.write("\x1e") - e.window.setTextColor(active_fg_bg.fgd) - e.window.setBackgroundColor(active_fg_bg.bkg) - e.window.setCursorPos(e.frame.w, e.frame.h) - e.window.write("\x1f") + e.w_set_fgd(nav_fg_bg.fgd) + e.w_set_bkg(nav_fg_bg.bkg) + e.w_set_cur(e.frame.w, 1) + e.w_write("\x1e") + e.w_set_fgd(active_fg_bg.fgd) + e.w_set_bkg(active_fg_bg.bkg) + e.w_set_cur(e.frame.w, e.frame.h) + e.w_write("\x1f") else - e.window.setTextColor(nav_fg_bg.fgd) - e.window.setBackgroundColor(nav_fg_bg.bkg) - e.window.setCursorPos(e.frame.w, 1) - e.window.write("\x1e") - e.window.setCursorPos(e.frame.w, e.frame.h) - e.window.write("\x1f") + e.w_set_fgd(nav_fg_bg.fgd) + e.w_set_bkg(nav_fg_bg.bkg) + e.w_set_cur(e.frame.w, 1) + e.w_write("\x1e") + e.w_set_cur(e.frame.w, e.frame.h) + e.w_write("\x1f") end - e.window.setTextColor(e.fg_bg.fgd) - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) end -- render the scroll bar and re-cacluate height & bounds @@ -115,23 +115,23 @@ local function listbox(args) for i = 2, e.frame.h - 1 do if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then if args.nav_fg_bg ~= nil then - e.window.setBackgroundColor(args.nav_fg_bg.fgd) + e.w_set_bkg(args.nav_fg_bg.fgd) else - e.window.setBackgroundColor(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.fgd) end else if args.nav_fg_bg ~= nil then - e.window.setBackgroundColor(args.nav_fg_bg.bkg) + e.w_set_bkg(args.nav_fg_bg.bkg) else - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.bkg) end end - e.window.setCursorPos(e.frame.w, i) - e.window.write(" ") + e.w_set_cur(e.frame.w, i) + e.w_write(" ") end - e.window.setBackgroundColor(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.bkg) end -- update item y positions and move elements @@ -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,16 +266,22 @@ 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 end - draw_arrows(0) - draw_bar() + -- element redraw + function e.redraw() + draw_arrows(0) + draw_bar() + end + + -- initial draw + e.redraw() return e.complete() end diff --git a/graphics/elements/multipane.lua b/graphics/elements/multipane.lua index fff8fb3..a283ed8 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") + element.assert(type(args.panes) == "table", "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 fe57757..625a70d 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") + element.assert(type(args.pipes) == "table", "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 @@ -73,7 +72,7 @@ local function pipenet(args) y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step) end - e.window.setCursorPos(x, y) + e.w_set_cur(x, y) local c = core.cpair(pipe.color, e.fg_bg.bkg) @@ -84,24 +83,24 @@ local function pipenet(args) if i == pipe.w then -- corner if y_step > 0 then - e.window.blit("\x93", c.blit_bkg, c.blit_fgd) + e.w_blit("\x93", c.blit_bkg, c.blit_fgd) else - e.window.blit("\x8e", c.blit_fgd, c.blit_bkg) + e.w_blit("\x8e", c.blit_fgd, c.blit_bkg) end else - e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) + e.w_blit("\x8c", c.blit_fgd, c.blit_bkg) end else if i == pipe.w and y_step > 0 then -- corner - e.window.blit(" ", c.blit_bkg, c.blit_fgd) + e.w_blit(" ", c.blit_bkg, c.blit_fgd) else - e.window.blit("\x8f", c.blit_fgd, c.blit_bkg) + e.w_blit("\x8f", c.blit_fgd, c.blit_bkg) end end x = x + x_step - e.window.setCursorPos(x, y) + e.w_set_cur(x, y) end -- back up one @@ -109,12 +108,12 @@ local function pipenet(args) for _ = 1, pipe.h - 1 do y = y + y_step - e.window.setCursorPos(x, y) + e.w_set_cur(x, y) if pipe.thin then - e.window.blit("\x95", c.blit_bkg, c.blit_fgd) + e.w_blit("\x95", c.blit_bkg, c.blit_fgd) else - e.window.blit(" ", c.blit_bkg, c.blit_fgd) + e.w_blit(" ", c.blit_bkg, c.blit_fgd) end end else @@ -124,26 +123,26 @@ local function pipenet(args) if i == pipe.h then -- corner if y_step < 0 then - e.window.blit("\x97", c.blit_bkg, c.blit_fgd) + e.w_blit("\x97", c.blit_bkg, c.blit_fgd) elseif y_step > 0 then - e.window.blit("\x8d", c.blit_fgd, c.blit_bkg) + e.w_blit("\x8d", c.blit_fgd, c.blit_bkg) else - e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) + e.w_blit("\x8c", c.blit_fgd, c.blit_bkg) end else - e.window.blit("\x95", c.blit_fgd, c.blit_bkg) + e.w_blit("\x95", c.blit_fgd, c.blit_bkg) end else if i == pipe.h and y_step < 0 then -- corner - e.window.blit("\x83", c.blit_bkg, c.blit_fgd) + e.w_blit("\x83", c.blit_bkg, c.blit_fgd) else - e.window.blit(" ", c.blit_bkg, c.blit_fgd) + e.w_blit(" ", c.blit_bkg, c.blit_fgd) end end y = y + y_step - e.window.setCursorPos(x, y) + e.w_set_cur(x, y) end -- back up one @@ -151,21 +150,119 @@ local function pipenet(args) for _ = 1, pipe.w - 1 do x = x + x_step - e.window.setCursorPos(x, y) + e.w_set_cur(x, y) if pipe.thin then - e.window.blit("\x8c", c.blit_fgd, c.blit_bkg) + e.w_blit("\x8c", c.blit_fgd, c.blit_bkg) else - e.window.blit("\x83", c.blit_bkg, c.blit_fgd) + e.w_blit("\x83", c.blit_bkg, c.blit_fgd) end 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.window.setCursorPos(x, y) - - if invert then - e.window.blit(char, entry.bg, entry.fg) - else - e.window.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 1d8afae..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, "graphics.elements.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 @@ -56,7 +56,7 @@ local function rectangle(args) -- draw bordered box if requested -- element constructor will have drawn basic colored rectangle regardless if args.border ~= nil then - e.window.setCursorPos(1, 1) + e.w_set_cur(1, 1) local border_width = offset_x local border_height = offset_y @@ -65,50 +65,50 @@ 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") + 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) - local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w) + local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w) local blit_fg_sides = blit_fg local blit_bg_sides = "" - local blit_bg_top_bot = util.strrep(border_blit, e.frame.w) + local blit_bg_top_bot = string.rep(border_blit, e.frame.w) -- partial bars local p_a, p_b, p_s if args.thin == true then if args.even_inner == true then - p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93" - p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e" + p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93" + p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e" else - p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94" - p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85" + p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94" + p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85" end p_s = "\x95" .. util.spaces(inner_width) .. "\x95" else if args.even_inner == true then - p_a = util.strrep("\x83", inner_width + width_x2) - p_b = util.strrep("\x8f", inner_width + width_x2) + p_a = string.rep("\x83", inner_width + width_x2) + p_b = string.rep("\x8f", inner_width + width_x2) else - p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width) - p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width) + p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width) + p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width) end p_s = spaces end - local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. - util.strrep(border_blit, border_width) - local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) .. - util.strrep(e.fg_bg.blit_bkg, border_width) + local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) .. + string.rep(border_blit, border_width) + local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) .. + string.rep(e.fg_bg.blit_bkg, border_width) if args.thin == true then - p_inv_fg = e.fg_bg.blit_bkg .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. util.strrep(border_blit, border_width) - p_inv_bg = border_blit .. util.strrep(border_blit, inner_width) .. util.strrep(e.fg_bg.blit_bkg, border_width) + p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width) + p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width) - blit_fg_sides = border_blit .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg + blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg end -- form the body blit strings (sides are border, inside is normal) @@ -126,64 +126,69 @@ local function rectangle(args) end -- draw rectangle with borders - for y = 1, e.frame.h do - e.window.setCursorPos(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.window.blit(p_a, p_inv_bg, p_inv_fg) - else - local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg) - local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg) + 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.window.blit(p_b, _fg, _bg) - elseif width_x2 % 3 == 2 then - e.window.blit(p_a, _fg, _bg) - else - -- skip line - e.window.blit(spaces, blit_fg, blit_bg_sides) - end - end - else - e.window.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.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w)) - else - e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) + 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, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot) - - if width_x2 % 3 == 1 then - e.window.blit(p_a, _fg, _bg) - elseif width_x2 % 3 == 2 then - e.window.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.window.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.window.blit(spaces, blit_fg, blit_bg_top_bot) - end - else - if args.thin == true then - e.window.blit(p_s, blit_fg_sides, blit_bg_sides) - else - e.window.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 521c499..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 @@ -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") + element.assert(type(args.text) == "string", "text is a required field") -- create new graphics element base object local e = element.new(args) - local alignment = args.alignment or TEXT_ALIGN.LEFT + e.value = args.text + + local alignment = args.alignment or 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 @@ -44,27 +45,28 @@ local function textbox(args) local len = string.len(lines[i]) -- use cursor position to align this line - if alignment == TEXT_ALIGN.CENTER then - e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i) - elseif alignment == TEXT_ALIGN.RIGHT then - e.window.setCursorPos((e.frame.w - len) + 1, i) + if alignment == ALIGN.CENTER then + e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i) + elseif alignment == ALIGN.RIGHT then + e.w_set_cur((e.frame.w - len) + 1, i) else - e.window.setCursorPos(1, i) + e.w_set_cur(1, i) end - e.window.write(lines[i]) + e.w_write(lines[i]) 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 4c0d0ba..02e2605 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") + element.assert(type(args.fill_c) == "table", "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.window.setBackgroundColor(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") + 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") - -- create pattern - for y = start_y, inner_height + (start_y - 1) do - e.window.setCursorPos(start_x, y) - for _ = 1, inner_width do - if alternator then - if even then - e.window.blit(" ", "00", fill_a .. fill_a) - else - e.window.blit(" ", "0", fill_a) - end - else - if even then - e.window.blit(" ", "00", fill_b .. fill_b) - else - e.window.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 diff --git a/graphics/events.lua b/graphics/events.lua index 3391a18..603dfee 100644 --- a/graphics/events.lua +++ b/graphics/events.lua @@ -4,46 +4,77 @@ 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 } ----@enum CLICK_TYPE -events.CLICK_TYPE = { +events.CLICK_BUTTON = CLICK_BUTTON + +---@enum MOUSE_CLICK +local MOUSE_CLICK = { TAP = 1, -- screen tap (complete click) DOWN = 2, -- button down 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 + +---@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 ---@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 ----@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, + -- double click tracking + dc_start = 0, + dc_step = 1, + dc_coord = _coord2d(0, 0) } -- create a new monitor touch mouse interaction event @@ -55,8 +86,8 @@ local handler = { local function _monitor_touch(monitor, x, y) return { monitor = monitor, - button = events.CLICK_BUTTON.GENERIC, - type = events.CLICK_TYPE.TAP, + button = CLICK_BUTTON.GENERIC, + type = MOUSE_CLICK.TAP, initial = _coord2d(x, y), current = _coord2d(x, y) } @@ -65,7 +96,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 @@ -83,14 +114,14 @@ 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 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) @@ -115,8 +146,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 @@ -126,35 +157,97 @@ function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events. ---@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) - return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, 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 - return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y) + + 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 - return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y) + 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, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP) - return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y) + local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP) + 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 --- 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 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/lockbox/digest/md5.lua b/lockbox/digest/md5.lua index 6ce1df2..b6ff36c 100644 --- a/lockbox/digest/md5.lua +++ b/lockbox/digest/md5.lua @@ -1,5 +1,3 @@ -require("lockbox").insecure(); - local Bit = require("lockbox.util.bit"); local String = require("string"); local Math = require("math"); diff --git a/lockbox/digest/sha1.lua b/lockbox/digest/sha1.lua index fc38866..3b13937 100644 --- a/lockbox/digest/sha1.lua +++ b/lockbox/digest/sha1.lua @@ -1,5 +1,3 @@ -require("lockbox").insecure(); - local Bit = require("lockbox.util.bit"); local String = require("string"); local Math = require("math"); diff --git a/lockbox/init.lua b/lockbox/init.lua index caee6c0..698c98e 100644 --- a/lockbox/init.lua +++ b/lockbox/init.lua @@ -1,25 +1,6 @@ -local Lockbox = {}; +local Lockbox = {} -- cc-mek-scada lockbox version -Lockbox.version = "1.0" +Lockbox.version = "1.1" ---[[ -package.path = "./?.lua;" - .. "./cipher/?.lua;" - .. "./digest/?.lua;" - .. "./kdf/?.lua;" - .. "./mac/?.lua;" - .. "./padding/?.lua;" - .. "./test/?.lua;" - .. "./util/?.lua;" - .. package.path; ---]] -Lockbox.ALLOW_INSECURE = true; - -Lockbox.insecure = function() - assert(Lockbox.ALLOW_INSECURE, - "This module is insecure! It should not be used in production." .. - "If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it"); -end - -return Lockbox; +return Lockbox diff --git a/lockbox/mac/hmac.lua b/lockbox/mac/hmac.lua index a10b84c..1a02b00 100644 --- a/lockbox/mac/hmac.lua +++ b/lockbox/mac/hmac.lua @@ -8,7 +8,7 @@ local HMAC = function() local public = {}; local blockSize = 64; - local Digest = nil; + local Digest; local outerPadding = {}; local innerPadding = {} local digest; diff --git a/lockbox/util/array.lua b/lockbox/util/array.lua index bd9ed56..b018a7b 100644 --- a/lockbox/util/array.lua +++ b/lockbox/util/array.lua @@ -1,4 +1,3 @@ - local String = require("string"); local Bit = require("lockbox.util.bit"); local Queue = require("lockbox.util.queue"); diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 65b286a..b2da51d 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -7,7 +7,7 @@ local iocontrol = require("pocket.iocontrol") local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE local LINK_STATE = iocontrol.LINK_STATE @@ -51,7 +51,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range nic.open(pkt_channel) -- send a management packet to the supervisor - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_sv(msg_type, msg) local s_pkt = comms.scada_packet() @@ -65,7 +65,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range end -- send a management packet to the coordinator - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_crd(msg_type, msg) local s_pkt = comms.scada_packet() @@ -80,24 +80,24 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range -- attempt supervisor connection establishment local function _send_sv_establish() - _send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) + _send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) end -- attempt coordinator API connection establishment local function _send_api_establish() - _send_crd(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) + _send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) end -- keep alive ack to supervisor ---@param srv_time integer local function _send_sv_keep_alive_ack(srv_time) - _send_sv(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) + _send_sv(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- keep alive ack to coordinator ---@param srv_time integer local function _send_api_keep_alive_ack(srv_time) - _send_crd(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) + _send_crd(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- PUBLIC FUNCTIONS -- @@ -111,7 +111,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range self.sv.linked = false self.sv.r_seq_num = nil self.sv.addr = comms.BROADCAST - _send_sv(SCADA_MGMT_TYPE.CLOSE, {}) + _send_sv(MGMT_TYPE.CLOSE, {}) end -- close connection to coordinator API server @@ -120,7 +120,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range self.api.linked = false self.api.r_seq_num = nil self.api.addr = comms.BROADCAST - _send_crd(SCADA_MGMT_TYPE.CLOSE, {}) + _send_crd(MGMT_TYPE.CLOSE, {}) end -- close the connections to the servers @@ -157,21 +157,21 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range -- supervisor get active alarm tones function public.diag__get_alarm_tones() - if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_GET, {}) end + if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_TONE_GET, {}) end end -- supervisor test alarm tones by tone ---@param id TONE|0 tone ID, or 0 to stop all ---@param state boolean tone state function public.diag__set_alarm_tone(id, state) - if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_SET, { id, state }) end + if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_TONE_SET, { id, state }) end end -- supervisor test alarm tones by alarm ---@param id ALARM|0 alarm ID, 0 to stop all ---@param state boolean alarm state function public.diag__set_alarm(id, state) - if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end + if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end end -- parse a packet @@ -180,7 +180,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range ---@param reply_to integer ---@param message any ---@param distance integer - ---@return mgmt_frame|capi_frame|nil packet + ---@return mgmt_frame|crdn_frame|nil packet function public.parse_packet(side, sender, reply_to, message, distance) local s_pkt = nic.receive(side, sender, reply_to, message, distance) local pkt = nil @@ -192,11 +192,11 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end - -- get as coordinator API packet - elseif s_pkt.protocol() == PROTOCOL.COORD_API then - local capi_pkt = comms.capi_packet() - if capi_pkt.decode(s_pkt) then - pkt = capi_pkt.get() + -- get as coordinator packet + elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then + local crdn_pkt = comms.crdn_packet() + if crdn_pkt.decode(s_pkt) then + pkt = crdn_pkt.get() end else log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) @@ -207,7 +207,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range end -- handle a packet - ---@param packet mgmt_frame|capi_frame|nil + ---@param packet mgmt_frame|crdn_frame|nil function public.handle_packet(packet) local diag = iocontrol.get_db().diag @@ -240,7 +240,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range if protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame if self.api.linked then - if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then local timestamp = packet.data[1] @@ -256,7 +256,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("coordinator SCADA keep alive packet length mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.CLOSE then + elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close api_watchdog.cancel() self.api.linked = false @@ -266,7 +266,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") end - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with coordinator established if packet.length == 1 then local est_ack = packet.data[1] @@ -330,7 +330,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range if protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame if self.sv.linked then - if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then local timestamp = packet.data[1] @@ -346,14 +346,14 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("supervisor SCADA keep alive packet length mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.CLOSE then + elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close sv_watchdog.cancel() self.sv.linked = false self.sv.r_seq_num = nil self.sv.addr = comms.BROADCAST log.info("supervisor server connection closed by remote host") - elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then + elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then if packet.length == 8 then for i = 1, #packet.data do diag.tone_test.tone_indicators[i].update(packet.data[i] == true) @@ -361,7 +361,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("supervisor SCADA diag alarm states packet length mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then + elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then if packet.length == 1 and packet.data[1] == false then diag.tone_test.ready_warn.set_value("testing denied") log.debug("supervisor SCADA diag tone set failed") @@ -380,7 +380,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("supervisor SCADA diag tone set packet length/type mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then + elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then if packet.length == 1 and packet.data[1] == false then diag.tone_test.ready_warn.set_value("testing denied") log.debug("supervisor SCADA diag alarm set failed") @@ -401,7 +401,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") end - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with supervisor established if packet.length == 1 then local est_ack = packet.data[1] 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 14ece9e..4929955 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.0-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 @@ -171,7 +170,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/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/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 new file mode 100644 index 0000000..7ad98a3 --- /dev/null +++ b/reactor-plc/configure.lua @@ -0,0 +1,687 @@ +-- +-- Configuration GUI +-- + +local log = require("scada-common.log") +local tcd = require("scada-common.tcd") +local util = require("scada-common.util") + +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 CheckBox = require("graphics.elements.controls.checkbox") +local PushButton = require("graphics.elements.controls.push_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") + +local println = util.println + +local cpair = core.cpair + +local LEFT = core.ALIGN.LEFT +local CENTER = core.ALIGN.CENTER +local RIGHT = core.ALIGN.RIGHT + +---@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.lightGray, hex = 0xcacaca }, + { c = colors.gray, hex = 0x575757 } +} + +local tool_ctl = { + ask_config = false, + has_config = false, + viewing_config = false, + importing_legacy = false, + + 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 + + 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 +local tmp_cfg = { + Networked = false, + UnitID = 0, + 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_config +local ini_cfg = {} + +local fields = { + { "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 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 } + +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) + for k, v in ipairs(side_options_map) do + if v == side then return k end + end +end + +-- 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 + end +end + +-- load data from the settings file +---@param target plc_config +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", log.MODE.APPEND) + 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) + +---@diagnostic disable-next-line: undefined-field + local function exit() 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} + + 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 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,summary}} + + -- MAIN PAGE + + 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 + 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() + 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_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_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 + + 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,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)} + + 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=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)} + + 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=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() + 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=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",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 + 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,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?",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()] + tmp_cfg.EmerCoolColor = color_options_map[color.get_value()] + 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 + + 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,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)} + + 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, 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)} + 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=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() + 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=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=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=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() + 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 timeout_val == 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=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,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=cpair(colors.black,colors.white)} + + 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() + 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=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 + + 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,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,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} + + local function submit_log() + 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 + + 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,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 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 + + ---@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("reactor-plc.settings") then + load_settings(ini_cfg) + + 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 + 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.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!"} + + 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=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)} + + 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.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 or false + + tool_ctl.gen_summary(tmp_cfg) + sum_pane.set_value(1) + main_pane.set_value(5) + 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) + setting_list.remove_all() + + 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 + local label_w = string.len(f[2]) + local val_max_w = (inner_width - label_w) + 1 + local raw = cfg[f[1]] + local val = util.strval(raw) + + 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 + + local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white)) + alternate = not alternate + + 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[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)} + + local textbox + if height > 1 then + textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT} + else + 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 + +-- 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("/reactor-plc.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 + + local status, error = pcall(function () + 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 + 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 + + 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 + + reset_term() + if not status then + println("configurator error: " .. error) + end + + return status, error +end + +return configurator diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index 91f8651..41737e8 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") @@ -23,15 +23,18 @@ 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 +local ind_grn = style.ind_grn +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) -- @@ -41,14 +44,14 @@ local function init(panel) local system = Div{parent=panel,width=14,height=18,x=2,y=3} local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} - local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} system.line_break() init_ok.register(databus.ps, "init_ok", init_ok.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update) local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} - local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} + local modem = LED{parent=system,label="MODEM",colors=ind_grn} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() @@ -57,11 +60,11 @@ local function init(panel) modem.register(databus.ps, "has_modem", modem.update) network.register(databus.ps, "link_state", network.update) - local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)} - local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)} - local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=cpair(colors.green,colors.green_off)} - local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=cpair(colors.green,colors.green_off)} - local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)} + local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn} + local rt_rps = LED{parent=system,label="RT RPS",colors=ind_grn} + local rt_cmtx = LED{parent=system,label="RT COMMS TX",colors=ind_grn} + local rt_cmrx = LED{parent=system,label="RT COMMS RX",colors=ind_grn} + local rt_sctl = LED{parent=system,label="RT SPCTL",colors=ind_grn} system.line_break() rt_main.register(databus.ps, "routine__main", rt_main.update) @@ -80,17 +83,17 @@ local function init(panel) local status = Div{parent=panel,width=19,height=18,x=17,y=3} - local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=cpair(colors.green,colors.green_off)} + local 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 local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)} local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=cpair(colors.red,colors.red_off),flash=true,period=flasher.PERIOD.BLINK_250_MS} + local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS} local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,colors.white,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)} local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=cpair(colors.black,colors.white)} @@ -105,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) @@ -116,20 +119,20 @@ local function init(panel) -- local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,colors.lightGray),thin=true,fg_bg=cpair(colors.black,colors.lightGray)} - local rps_man = LED{parent=rps,label="MANUAL",colors=cpair(colors.red,colors.red_off)} - local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=cpair(colors.red,colors.red_off)} - local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=cpair(colors.red,colors.red_off)} - local rps_flt = LED{parent=rps,label="PLC FAULT",colors=cpair(colors.red,colors.red_off)} - local rps_fail = LED{parent=rps,label="RCT FAULT",colors=cpair(colors.red,colors.red_off)} + local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red} + local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red} + local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red} + local rps_flt = LED{parent=rps,label="PLC FAULT",colors=ind_red} + local rps_fail = LED{parent=rps,label="RCT FAULT",colors=ind_red} rps.line_break() - local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=cpair(colors.red,colors.red_off)} - local rps_tmp = LED{parent=rps,label="HI TEMP",colors=cpair(colors.red,colors.red_off)} + local rps_dmg = LED{parent=rps,label="HI DAMAGE",colors=ind_red} + local rps_tmp = LED{parent=rps,label="HI TEMP",colors=ind_red} rps.line_break() - local rps_nof = LED{parent=rps,label="LO FUEL",colors=cpair(colors.red,colors.red_off)} - local rps_wst = LED{parent=rps,label="HI WASTE",colors=cpair(colors.red,colors.red_off)} + local rps_nof = LED{parent=rps,label="LO FUEL",colors=ind_red} + local rps_wst = LED{parent=rps,label="HI WASTE",colors=ind_red} rps.line_break() - local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)} - local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)} + local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=ind_red} + local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=ind_red} rps_man.register(databus.ps, "rps_manual", rps_man.update) rps_auto.register(databus.ps, "rps_automatic", rps_auto.update) diff --git a/reactor-plc/panel/style.lua b/reactor-plc/panel/style.lua index edf3f64..1cf783c 100644 --- a/reactor-plc/panel/style.lua +++ b/reactor-plc/panel/style.lua @@ -39,4 +39,9 @@ style.colors = { { c = colors.brown, hex = 0x672223 } -- RED OFF } +-- COMMON COLOR PAIRS -- + +style.ind_grn = cpair(colors.green, colors.green_off) +style.ind_red = cpair(colors.red, colors.red_off) + return style diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index acdc78b..02d7c3d 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -16,7 +16,7 @@ local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK local RPLC_TYPE = comms.RPLC_TYPE -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE local AUTO_ACK = comms.PLC_AUTO_ACK local RPS_LIMITS = const.RPS_LIMITS @@ -26,14 +26,61 @@ 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() + if not settings.load("/reactor-plc.settings") then return false end + + 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_min(config.TrustedRange, 0) + 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 +120,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 @@ -238,8 +285,9 @@ function plc.rps_init(reactor, is_formed, emer_cool) self.state[state_keys.sys_fail] = true end - -- SCRAM the reactor now (blocks waiting for server tick) + -- SCRAM the reactor now
---@return boolean success + --- EVENT_CONSUMER: this function consumes events function public.scram() log.info("RPS: reactor SCRAM") @@ -254,8 +302,9 @@ function plc.rps_init(reactor, is_formed, emer_cool) end end - -- start the reactor now (blocks waiting for server tick) + -- start the reactor now
---@return boolean success + --- EVENT_CONSUMER: this function consumes events function public.activate() if not self.tripped then log.info("RPS: reactor start") @@ -443,16 +492,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 +511,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,15 +526,15 @@ 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 -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() @@ -498,7 +543,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 @@ -600,7 +645,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) - _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) + _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack @@ -612,10 +657,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r -- send structure properties (these should not change, server will cache these) local function _send_struct() - local min_pos = { x = 0, y = 0, z = 0 } - local max_pos = { x = 0, y = 0, z = 0 } - - local mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 } + local mek_data = { false, 0, 0, 0, types.new_zero_coordinate(), types.new_zero_coordinate(), 0, 0, 0, 0, 0, 0, 0, 0 } local tasks = { function () mek_data[1] = reactor.getLength() end, @@ -668,12 +710,12 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r function public.close() conn_watchdog.cancel() public.unlink() - _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + _send_mgmt(MGMT_TYPE.CLOSE, {}) end -- attempt to establish link with supervisor function public.send_link_req() - _send_mgmt(SCADA_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 @@ -685,21 +727,18 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r local heating_rate = 0.0 ---@type number if (not no_reactor) and rps.is_formed() then - if _update_status_cache() then - mek_data = self.status_cache - end - + if _update_status_cache() then mek_data = self.status_cache end heating_rate = reactor.getHeatingRate() end local sys_status = { - util.time(), -- timestamp - (not self.scrammed), -- requested control state - no_reactor, -- no reactor peripheral connected - formed, -- reactor formed - self.auto_ack_token, -- token to indicate auto command has been received before this status update - heating_rate, -- heating rate - mek_data -- mekanism status data + util.time(), -- timestamp + (not self.scrammed), -- requested control state + no_reactor, -- no reactor peripheral connected + formed, -- reactor formed + self.auto_ack_token, -- indicate auto command received prior to this status update + heating_rate, -- heating rate + mek_data -- mekanism status data } _send(RPLC_TYPE.STATUS, sys_status) @@ -769,7 +808,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() @@ -837,6 +876,10 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r -- enable the reactor self.scrammed = false _send_ack(packet.type, rps.activate()) + elseif packet.type == RPLC_TYPE.RPS_DISABLE then + -- disable the reactor, but do not trip + self.scrammed = true + _send_ack(packet.type, rps.scram()) elseif packet.type == RPLC_TYPE.RPS_SCRAM then -- disable the reactor per manual request self.scrammed = true @@ -929,7 +972,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r ---@cast packet mgmt_frame -- if linked, only accept packets from configured supervisor if self.linked then - if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 and type(packet.data[1]) == "number" then local timestamp = packet.data[1] @@ -945,7 +988,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r else log.debug("SCADA_MGMT keep alive packet length/type mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.CLOSE then + elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close conn_watchdog.cancel() public.unlink() @@ -954,7 +997,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r else log.debug("received unsupported SCADA_MGMT packet type " .. packet.type) end - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- link request confirmation if packet.length == 1 then local est_ack = packet.data[1] 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 2c58637..7ad0d27 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.7" +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 @@ -184,10 +172,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)) @@ -197,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") @@ -216,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 diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 3b06904..6699ea3 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/modbus.lua b/rtu/modbus.lua index 06c1273..44900a0 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -215,7 +215,7 @@ function modbus.new(rtu_dev, use_parallel_read) ---@param value any ---@return boolean ok, MODBUS_EXCODE local function _5_write_single_coil(c_addr, value) - local response = nil + local response = MODBUS_EXCODE.OK local _, coils, _, _ = rtu_dev.io_count() local return_ok = c_addr <= coils @@ -239,7 +239,7 @@ function modbus.new(rtu_dev, use_parallel_read) ---@param value any ---@return boolean ok, MODBUS_EXCODE local function _6_write_single_holding_register(hr_addr, value) - local response = nil + local response = MODBUS_EXCODE.OK local _, _, _, hold_regs = rtu_dev.io_count() local return_ok = hr_addr <= hold_regs @@ -263,7 +263,7 @@ function modbus.new(rtu_dev, use_parallel_read) ---@param values any ---@return boolean ok, MODBUS_EXCODE local function _15_write_multiple_coils(c_addr_start, values) - local response = nil + local response = MODBUS_EXCODE.OK local _, coils, _, _ = rtu_dev.io_count() local count = #values local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) @@ -292,7 +292,7 @@ function modbus.new(rtu_dev, use_parallel_read) ---@param values any ---@return boolean ok, MODBUS_EXCODE local function _16_write_multiple_holding_registers(hr_addr_start, values) - local response = nil + local response = MODBUS_EXCODE.OK local _, _, _, hold_regs = rtu_dev.io_count() local count = #values local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) @@ -403,7 +403,7 @@ function modbus.new(rtu_dev, use_parallel_read) end if type(response) == "table" then - elseif type(response) == "nil" then + elseif response == MODBUS_EXCODE.OK then response = {} else response = { response } diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index ad6f7a0..2e5537b 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -18,18 +18,21 @@ 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 -local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" } +local fp_label = style.fp_label +local ind_grn = style.ind_grn + +local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" } -- create new front panel view ---@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 @@ -38,13 +41,13 @@ local function init(panel, units) local system = Div{parent=panel,width=14,height=18,x=2,y=3} local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} - local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} on.update(true) system.line_break() heartbeat.register(databus.ps, "heartbeat", heartbeat.update) - local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} + local modem = LED{parent=system,label="MODEM",colors=ind_grn} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() @@ -52,8 +55,8 @@ local function init(panel, units) modem.register(databus.ps, "has_modem", modem.update) network.register(databus.ps, "link_state", network.update) - local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)} - local rt_comm = LED{parent=system,label="RT COMMS",colors=cpair(colors.green,colors.green_off)} + local rt_main = LED{parent=system,label="RT MAIN",colors=ind_grn} + local rt_comm = LED{parent=system,label="RT COMMS",colors=ind_grn} system.line_break() rt_main.register(databus.ps, "routine__main", rt_main.update) @@ -61,7 +64,7 @@ local function init(panel, units) ---@diagnostic disable-next-line: undefined-field local comp_id = util.sprintf("(%d)", os.getComputerID()) - TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=fp_label} TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.label} local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=cpair(colors.gray,colors.white)} @@ -71,9 +74,9 @@ local function init(panel, units) -- about label -- - local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=cpair(colors.lightGray,colors.ivory)} - local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} - local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} + 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=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) @@ -90,7 +93,7 @@ local function init(panel, units) -- show routine statuses for i = 1, list_length do TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1} - local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=cpair(colors.green,colors.green_off)} + local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn} rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update) end @@ -115,7 +118,7 @@ local function init(panel, units) -- assignment (unit # or facility) local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) - TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=cpair(colors.lightGray,colors.ivory)} + TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,height=1,fg_bg=fp_label} end end diff --git a/rtu/panel/style.lua b/rtu/panel/style.lua index edf3f64..bfc52cf 100644 --- a/rtu/panel/style.lua +++ b/rtu/panel/style.lua @@ -39,4 +39,10 @@ style.colors = { { c = colors.brown, hex = 0x672223 } -- RED OFF } +-- COMMON COLOR PAIRS -- + +style.fp_label = cpair(colors.lightGray, colors.ivory) + +style.ind_grn = cpair(colors.green, colors.green_off) + return style 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/rtu.lua b/rtu/rtu.lua index 4c92afc..242bd18 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -14,7 +14,7 @@ local rtu = {} local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -- create a new RTU unit @@ -227,7 +227,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) nic.open(rtu_channel) -- send a scada management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send(msg_type, msg) local s_pkt = comms.scada_packet() @@ -243,7 +243,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) -- keep alive ack ---@param srv_time integer local function _send_keep_alive_ack(srv_time) - _send(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) + _send(MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() }) end -- generate device advertisement table @@ -298,25 +298,25 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) function public.close(rtu_state) conn_watchdog.cancel() public.unlink(rtu_state) - _send(SCADA_MGMT_TYPE.CLOSE, {}) + _send(MGMT_TYPE.CLOSE, {}) end -- send establish request (includes advertisement) ---@param units table function public.send_establish(units) - _send(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) }) + _send(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) }) end -- send capability advertisement ---@param units table function public.send_advertisement(units) - _send(SCADA_MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units)) + _send(MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units)) end -- notify that a peripheral was remounted ---@param unit_index integer RTU unit ID function public.send_remounted(unit_index) - _send(SCADA_MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index }) + _send(MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index }) end -- parse a MODBUS/SCADA packet @@ -433,7 +433,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) ---@cast packet mgmt_frame -- SCADA management packet if rtu_state.linked then - if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 and type(packet.data[1]) == "number" then local timestamp = packet.data[1] @@ -449,16 +449,16 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) else log.debug("SCADA_MGMT keep alive packet length/type mismatch") end - elseif packet.type == SCADA_MGMT_TYPE.CLOSE then + elseif packet.type == MGMT_TYPE.CLOSE then -- close connection conn_watchdog.cancel() public.unlink(rtu_state) println_ts("server connection closed by remote host") log.warning("server connection closed by remote host") - elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then + elseif packet.type == MGMT_TYPE.RTU_ADVERT then -- request for capabilities again public.send_advertisement(units) - elseif packet.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM then + elseif packet.type == MGMT_TYPE.RTU_TONE_ALARM then -- alarm tone update from supervisor if (packet.length == 1) and type(packet.data[1] == "table") and (#packet.data[1] == 8) then local states = packet.data[1] @@ -474,7 +474,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) -- not supported log.debug("received unsupported SCADA_MGMT message type " .. packet.type) end - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then if packet.length == 1 then local est_ack = packet.data[1] diff --git a/rtu/startup.lua b/rtu/startup.lua index 02679c2..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.2" +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/rtu/threads.lua b/rtu/threads.lua index bed6294..d76d062 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/scada-common/comms.lua b/scada-common/comms.lua index af049d7..4bf8262 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -6,23 +6,25 @@ local log = require("scada-common.log") local insert = table.insert +---@type integer computer ID ---@diagnostic disable-next-line: undefined-field -local COMPUTER_ID = os.getComputerID() ---@type integer computer ID +local COMPUTER_ID = os.getComputerID() -local max_distance = nil ---@type number|nil maximum acceptable transmission distance +---@type number|nil maximum acceptable transmission distance +local max_distance = nil ---@class comms local comms = {} -comms.version = "2.2.1" +-- protocol version (non-protocol changes tracked by util.lua version) +comms.version = "2.4.0" ---@enum PROTOCOL local PROTOCOL = { - MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol + MODBUS_TCP = 0, -- the "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc - SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers - COORD_API = 4 -- data/control packets for pocket computers to/from coordinators + SCADA_CRDN = 3 -- data/control packets for coordinators to/from supervisory controllers } ---@enum RPLC_TYPE @@ -31,17 +33,18 @@ local RPLC_TYPE = { MEK_STRUCT = 1, -- mekanism build structure MEK_BURN_RATE = 2, -- set burn rate RPS_ENABLE = 3, -- enable reactor - RPS_SCRAM = 4, -- SCRAM reactor (manual request) - RPS_ASCRAM = 5, -- SCRAM reactor (automatic request) - RPS_STATUS = 6, -- RPS status - RPS_ALARM = 7, -- RPS alarm broadcast - RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately) - RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram - AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited + RPS_DISABLE = 4, -- disable the reactor + RPS_SCRAM = 5, -- SCRAM reactor (manual request) + RPS_ASCRAM = 6, -- SCRAM reactor (automatic request) + RPS_STATUS = 7, -- RPS status + RPS_ALARM = 8, -- RPS alarm broadcast + RPS_RESET = 9, -- clear RPS trip (if in bad state, will trip immediately) + RPS_AUTO_RESET = 10, -- clear RPS trip if it is just a timeout or auto scram + AUTO_BURN_RATE = 11 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited } ----@enum SCADA_MGMT_TYPE -local SCADA_MGMT_TYPE = { +---@enum MGMT_TYPE +local MGMT_TYPE = { ESTABLISH = 0, -- establish new connection KEEP_ALIVE = 1, -- keep alive packet w/ RTT CLOSE = 2, -- close a connection @@ -53,8 +56,8 @@ local SCADA_MGMT_TYPE = { DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for } ----@enum SCADA_CRDN_TYPE -local SCADA_CRDN_TYPE = { +---@enum CRDN_TYPE +local CRDN_TYPE = { INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator FAC_BUILDS = 1, -- facility RTU builds FAC_STATUS = 2, -- state of facility and facility devices @@ -64,10 +67,6 @@ local SCADA_CRDN_TYPE = { UNIT_CMD = 6 -- command a reactor unit } ----@enum CAPI_TYPE -local CAPI_TYPE = { -} - ---@enum ESTABLISH_ACK local ESTABLISH_ACK = { ALLOW = 0, -- link approved @@ -76,14 +75,8 @@ local ESTABLISH_ACK = { BAD_VERSION = 3 -- link denied due to comms version mismatch } ----@enum DEVICE_TYPE -local DEVICE_TYPE = { - PLC = 0, -- PLC device type for establish - RTU = 1, -- RTU device type for establish - SV = 2, -- supervisor device type for establish - CRDN = 3, -- coordinator device type for establish - PKT = 4 -- pocket device type for establish -} +---@enum DEVICE_TYPE device types for establish messages +local DEVICE_TYPE = { PLC = 0, RTU = 1, SVR = 2, CRD = 3, PKT = 4 } ---@enum PLC_AUTO_ACK local PLC_AUTO_ACK = { @@ -119,9 +112,8 @@ local UNIT_COMMAND = { comms.PROTOCOL = PROTOCOL comms.RPLC_TYPE = RPLC_TYPE -comms.SCADA_MGMT_TYPE = SCADA_MGMT_TYPE -comms.SCADA_CRDN_TYPE = SCADA_CRDN_TYPE -comms.CAPI_TYPE = CAPI_TYPE +comms.MGMT_TYPE = MGMT_TYPE +comms.CRDN_TYPE = CRDN_TYPE comms.ESTABLISH_ACK = ESTABLISH_ACK comms.DEVICE_TYPE = DEVICE_TYPE @@ -134,8 +126,8 @@ comms.FAC_COMMAND = FAC_COMMAND -- destination broadcast address (to all devices) comms.BROADCAST = -1 ----@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ----@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame +---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet +---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame -- configure the maximum allowable message receive distance
-- packets received with distances greater than this will be silently discarded @@ -144,7 +136,7 @@ function comms.set_trusted_range(distance) if distance == 0 then max_distance = nil else max_distance = distance end end --- generic SCADA packet object +-- generic SCADA packet ---@nodiscard function comms.scada_packet() local self = { @@ -199,9 +191,9 @@ function comms.scada_packet() self.valid = false self.raw = self.modem_msg_in.msg - if (type(max_distance) == "number") and (distance > max_distance) then + if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then -- outside of maximum allowable transmission distance - -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range") + -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") else if type(self.raw) == "table" then if #self.raw == 5 then @@ -227,12 +219,8 @@ function comms.scada_packet() -- check if this packet is destined for this device local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID) - self.valid = is_destination and - type(self.src_addr) == "number" and - type(self.dest_addr) == "number" and - type(self.seq_num) == "number" and - type(self.protocol) == "number" and - type(self.payload) == "table" + self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and + type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table" end end @@ -275,7 +263,7 @@ function comms.scada_packet() return public end --- authenticated SCADA packet object +-- authenticated SCADA packet ---@nodiscard function comms.authd_packet() local self = { @@ -325,7 +313,7 @@ function comms.authd_packet() if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then -- outside of maximum allowable transmission distance - -- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range") + -- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") else if type(self.raw) == "table" then if #self.raw == 4 then @@ -343,11 +331,8 @@ function comms.authd_packet() -- check if this packet is destined for this device local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID) - self.valid = is_destination and - type(self.src_addr) == "number" and - type(self.dest_addr) == "number" and - type(self.mac) == "string" and - type(self.payload) == "string" + self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and + type(self.mac) == "string" and type(self.payload) == "string" end end @@ -381,8 +366,7 @@ function comms.authd_packet() return public end --- MODBUS packet
--- modeled after MODBUS TCP packet +-- MODBUS packet, modeled after MODBUS TCP ---@nodiscard function comms.modbus_packet() local self = { @@ -413,9 +397,7 @@ function comms.modbus_packet() -- populate raw array self.raw = { self.txn_id, self.unit_id, self.func_code } - for i = 1, self.length do - insert(self.raw, data[i]) - end + for i = 1, self.length do insert(self.raw, data[i]) end else log.error("comms.modbus_packet.make(): data not table") end @@ -436,9 +418,7 @@ function comms.modbus_packet() public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) end - local valid = type(self.txn_id) == "number" and - type(self.unit_id) == "number" and - type(self.func_code) == "number" + local valid = type(self.txn_id) == "number" and type(self.unit_id) == "number" and type(self.func_code) == "number" return size_ok and valid else @@ -489,21 +469,6 @@ function comms.rplc_packet() ---@class rplc_packet local public = {} - -- check that type is known - local function _rplc_type_valid() - return self.type == RPLC_TYPE.STATUS or - self.type == RPLC_TYPE.MEK_STRUCT or - self.type == RPLC_TYPE.MEK_BURN_RATE or - self.type == RPLC_TYPE.RPS_ENABLE or - self.type == RPLC_TYPE.RPS_SCRAM or - self.type == RPLC_TYPE.RPS_ASCRAM or - self.type == RPLC_TYPE.RPS_STATUS or - self.type == RPLC_TYPE.RPS_ALARM or - self.type == RPLC_TYPE.RPS_RESET or - self.type == RPLC_TYPE.RPS_AUTO_RESET or - self.type == RPLC_TYPE.AUTO_BURN_RATE - end - -- make an RPLC packet ---@param id integer ---@param packet_type RPLC_TYPE @@ -518,9 +483,7 @@ function comms.rplc_packet() -- populate raw array self.raw = { self.id, self.type } - for i = 1, #data do - insert(self.raw, data[i]) - end + for i = 1, #data do insert(self.raw, data[i]) end else log.error("comms.rplc_packet.make(): data not table") end @@ -539,7 +502,6 @@ function comms.rplc_packet() if ok then local data = frame.data() public.make(data[1], data[2], { table.unpack(data, 3, #data) }) - ok = _rplc_type_valid() end ok = ok and type(self.id) == "number" @@ -583,7 +545,7 @@ function comms.mgmt_packet() local self = { frame = nil, raw = {}, - type = 0, ---@type SCADA_MGMT_TYPE + type = 0, ---@type MGMT_TYPE length = 0, data = {} } @@ -591,22 +553,8 @@ function comms.mgmt_packet() ---@class mgmt_packet local public = {} - -- check that type is known - local function _scada_type_valid() - return self.type == SCADA_MGMT_TYPE.ESTABLISH or - self.type == SCADA_MGMT_TYPE.KEEP_ALIVE or - self.type == SCADA_MGMT_TYPE.CLOSE or - self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or - self.type == SCADA_MGMT_TYPE.RTU_ADVERT or - self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT or - self.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM or - self.type == SCADA_MGMT_TYPE.DIAG_TONE_GET or - self.type == SCADA_MGMT_TYPE.DIAG_TONE_SET or - self.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET - end - -- make a SCADA management packet - ---@param packet_type SCADA_MGMT_TYPE + ---@param packet_type MGMT_TYPE ---@param data table function public.make(packet_type, data) if type(data) == "table" then @@ -617,9 +565,7 @@ function comms.mgmt_packet() -- populate raw array self.raw = { self.type } - for i = 1, #data do - insert(self.raw, data[i]) - end + for i = 1, #data do insert(self.raw, data[i]) end else log.error("comms.mgmt_packet.make(): data not table") end @@ -638,7 +584,6 @@ function comms.mgmt_packet() if ok then local data = frame.data() public.make(data[1], { table.unpack(data, 2, #data) }) - ok = _scada_type_valid() end return ok @@ -679,7 +624,7 @@ function comms.crdn_packet() local self = { frame = nil, raw = {}, - type = 0, ---@type SCADA_CRDN_TYPE + type = 0, ---@type CRDN_TYPE length = 0, data = {} } @@ -687,20 +632,8 @@ function comms.crdn_packet() ---@class crdn_packet local public = {} - -- check that type is known - ---@nodiscard - local function _crdn_type_valid() - return self.type == SCADA_CRDN_TYPE.INITIAL_BUILDS or - self.type == SCADA_CRDN_TYPE.FAC_BUILDS or - self.type == SCADA_CRDN_TYPE.FAC_STATUS or - self.type == SCADA_CRDN_TYPE.FAC_CMD or - self.type == SCADA_CRDN_TYPE.UNIT_BUILDS or - self.type == SCADA_CRDN_TYPE.UNIT_STATUSES or - self.type == SCADA_CRDN_TYPE.UNIT_CMD - end - -- make a coordinator packet - ---@param packet_type SCADA_CRDN_TYPE + ---@param packet_type CRDN_TYPE ---@param data table function public.make(packet_type, data) if type(data) == "table" then @@ -711,9 +644,7 @@ function comms.crdn_packet() -- populate raw array self.raw = { self.type } - for i = 1, #data do - insert(self.raw, data[i]) - end + for i = 1, #data do insert(self.raw, data[i]) end else log.error("comms.crdn_packet.make(): data not table") end @@ -732,7 +663,6 @@ function comms.crdn_packet() if ok then local data = frame.data() public.make(data[1], { table.unpack(data, 2, #data) }) - ok = _crdn_type_valid() end return ok @@ -767,92 +697,4 @@ function comms.crdn_packet() return public end --- coordinator API (CAPI) packet ----@todo implement for pocket access, set enum type for self.type ----@nodiscard -function comms.capi_packet() - local self = { - frame = nil, - raw = {}, - type = 0, - length = 0, - data = {} - } - - ---@class capi_packet - local public = {} - - local function _capi_type_valid() - ---@todo - return false - end - - -- make a coordinator API packet - ---@param packet_type CAPI_TYPE - ---@param data table - function public.make(packet_type, data) - if type(data) == "table" then - -- packet accessor properties - self.type = packet_type - self.length = #data - self.data = data - - -- populate raw array - self.raw = { self.type } - for i = 1, #data do - insert(self.raw, data[i]) - end - else - log.error("comms.capi_packet.make(): data not table") - end - end - - -- decode a coordinator API packet from a SCADA frame - ---@param frame scada_packet - ---@return boolean success - function public.decode(frame) - if frame then - self.frame = frame - - if frame.protocol() == PROTOCOL.COORD_API then - local ok = frame.length() >= 1 - - if ok then - local data = frame.data() - public.make(data[1], { table.unpack(data, 2, #data) }) - ok = _capi_type_valid() - end - - return ok - else - log.debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true) - return false - end - else - log.debug("nil frame encountered", true) - return false - end - end - - -- get raw to send - ---@nodiscard - function public.raw_sendable() return self.raw end - - -- get this packet as a frame with an immutable relation to this object - ---@nodiscard - function public.get() - ---@class capi_frame - local frame = { - scada_frame = self.frame, - type = self.type, - length = self.length, - data = self.data - } - - return frame - end - - return public -end - return comms diff --git a/scada-common/log.lua b/scada-common/log.lua index 2c266bb..8a42e45 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -16,7 +16,7 @@ local logger = { path = "/log.txt", mode = MODE.APPEND, debug = false, - file = nil, + file = nil, ---@type table|nil dmesg_out = nil, dmesg_restore_coord = { 1, 1 }, dmesg_scroll_count = 0 @@ -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/types.lua b/scada-common/types.lua index 91e6469..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" @@ -375,6 +376,7 @@ types.MODBUS_FCODE = { -- MODBUS exception codes ---@enum MODBUS_EXCODE types.MODBUS_EXCODE = { + OK = 0x00, ILLEGAL_FUNCTION = 0x01, ILLEGAL_DATA_ADDR = 0x02, ILLEGAL_DATA_VALUE = 0x03, diff --git a/scada-common/util.lua b/scada-common/util.lua index ea53575..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.0.2" +util.version = "1.1.2" -- ENVIRONMENT CONSTANTS -- @@ -76,25 +76,12 @@ function util.strval(val) end end --- repeat a string n times ----@nodiscard ----@param str string ----@param n integer ----@return string -function util.strrep(str, n) - local repeated = "" - - for _ = 1, n do repeated = repeated .. str end - - return repeated -end - -- repeat a space n times ---@nodiscard ---@param n integer ---@return string function util.spaces(n) - return util.strrep(" ", n) + return string.rep(" ", n) end -- pad text to a minimum width 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 diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 8c91157..30bcd68 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -399,10 +399,9 @@ function facility.new(num_reactors, cooling_conf) end elseif self.mode == PROCESS.INACTIVE then for i = 1, #self.prio_defs do - -- SCRAM reactors and disengage auto control - -- use manual SCRAM since inactive was requested, and automatic SCRAM trips an alarm + -- disable reactors and disengage auto control for _, u in pairs(self.prio_defs[i]) do - u.scram() + u.disable() u.auto_disengage() end end diff --git a/supervisor/panel/components/pdg_entry.lua b/supervisor/panel/components/pdg_entry.lua index 1e49fca..6cb07f7 100644 --- a/supervisor/panel/components/pdg_entry.lua +++ b/supervisor/panel/components/pdg_entry.lua @@ -4,6 +4,8 @@ local databus = require("supervisor.databus") +local style = require("supervisor.panel.style") + local core = require("graphics.core") local Div = require("graphics.elements.div") @@ -11,32 +13,35 @@ 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 +local black_lg = style.black_lg +local lg_white = style.lg_white + -- create a pocket diagnostics list entry ---@param parent graphics_element parent ---@param id integer PDG session ID local function init(parent, id) -- root div local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} - local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} + local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg} local ps_prefix = "pdg_" .. id .. "_" - TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local pdg_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} - TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + 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=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) TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1} - local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_white} pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value) TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1} - local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_white} + TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_white} pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update) pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor) diff --git a/supervisor/panel/components/rtu_entry.lua b/supervisor/panel/components/rtu_entry.lua index d9634e9..240c596 100644 --- a/supervisor/panel/components/rtu_entry.lua +++ b/supervisor/panel/components/rtu_entry.lua @@ -4,6 +4,8 @@ local databus = require("supervisor.databus") +local style = require("supervisor.panel.style") + local core = require("graphics.core") local Div = require("graphics.elements.div") @@ -11,36 +13,39 @@ 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 +local black_lg = style.black_lg +local lg_white = style.lg_white + -- create an RTU list entry ---@param parent graphics_element parent ---@param id integer RTU session ID local function init(parent, id) -- root div local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true} - local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)} + local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg} local ps_prefix = "rtu_" .. id .. "_" - TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - local rtu_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)} - TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + 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=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) TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1} - local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=cpair(colors.gray,colors.white)} + local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.gray_white} unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value) TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1} - local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=lg_white} rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value) TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1} - local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_white} + TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_white} rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update) rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor) diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua index 4ae6ddd..981dd5e 100644 --- a/supervisor/panel/front_panel.lua +++ b/supervisor/panel/front_panel.lua @@ -25,14 +25,22 @@ 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 +local bw_fg_bg = style.bw_fg_bg + +local black_lg = style.black_lg +local lg_white = style.lg_white +local gry_wht = style.gray_white + +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} @@ -45,28 +53,28 @@ local function init(panel) local system = Div{parent=main_page,width=14,height=17,x=2,y=2} local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} - local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)} + local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} on.update(true) system.line_break() heartbeat.register(databus.ps, "heartbeat", heartbeat.update) - local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)} + local modem = LED{parent=system,label="MODEM",colors=ind_grn} system.line_break() modem.register(databus.ps, "has_modem", modem.update) ---@diagnostic disable-next-line: undefined-field local comp_id = util.sprintf("(%d)", os.getComputerID()) - TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label} -- -- about footer -- - local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)} - local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} - local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1} + 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=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) @@ -82,25 +90,25 @@ local function init(panel) for i = 1, config.NUM_REACTORS do local ps_prefix = "plc_" .. i .. "_" - local plc_entry = Div{parent=plc_list,height=3,fg_bg=cpair(colors.black,colors.white)} + 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=cpair(colors.black,colors.lightGray)} - TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} - TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)} + 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=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=cpair(colors.green,colors.green_off)} + local conn = LED{parent=plc_entry,x=10,y=2,label="LINK",colors=ind_grn} conn.register(databus.ps, ps_prefix .. "conn", conn.update) - local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} + local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=gry_wht} plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value) TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1} - local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=lg_white} plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value) TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1} - local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=lg_white} + TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=lg_white} plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update) plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor) @@ -116,22 +124,22 @@ local function init(panel) -- coordinator page local crd_page = Div{parent=page_div,x=1,y=1,hidden=true} - local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=cpair(colors.black,colors.white)} + local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=bw_fg_bg} - local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)} + local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=ind_grn} crd_conn.register(databus.ps, "crd_conn", crd_conn.update) - TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)} - local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=crd_box,x=4,y=3,text="COMPUTER",width=8,height=1,fg_bg=gry_wht} + local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=gry_wht} crd_addr.register(databus.ps, "crd_addr", crd_addr.set_value) TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1} - local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=lg_white} crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value) TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1} - local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)} - TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)} + local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_white} + TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=lg_white} crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update) crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor) @@ -155,7 +163,7 @@ local function init(panel) { name = "PKT", color = cpair(colors.black, colors.ivory) }, } - TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)} + TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=bw_fg_bg} -- link RTU/PDG list management to PGI pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry) diff --git a/supervisor/panel/style.lua b/supervisor/panel/style.lua index 0668ccc..49bfb4f 100644 --- a/supervisor/panel/style.lua +++ b/supervisor/panel/style.lua @@ -39,4 +39,16 @@ style.colors = { { c = colors.brown, hex = 0x672223 } -- RED OFF } +-- COMMON COLOR PAIRS -- + +style.text_fg_bg = cpair(colors.black, colors.ivory) +style.bw_fg_bg = cpair(colors.black, colors.white) +style.fp_label = cpair(colors.lightGray, colors.ivory) + +style.black_lg = cpair(colors.black, colors.lightGray) +style.lg_white = cpair(colors.lightGray, colors.white) +style.gray_white = cpair(colors.gray, colors.white) + +style.ind_grn = cpair(colors.green, colors.green_off) + return style 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/session/coordinator.lua b/supervisor/session/coordinator.lua index aa64e17..98edaca 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -10,8 +10,8 @@ local svqtypes = require("supervisor.session.svqtypes") local coordinator = {} local PROTOCOL = comms.PROTOCOL -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE -local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE +local MGMT_TYPE = comms.MGMT_TYPE +local CRDN_TYPE = comms.CRDN_TYPE local UNIT_COMMAND = comms.UNIT_COMMAND local FAC_COMMAND = comms.FAC_COMMAND @@ -94,7 +94,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil end -- send a CRDN packet - ---@param msg_type SCADA_CRDN_TYPE + ---@param msg_type CRDN_TYPE ---@param msg table local function _send(msg_type, msg) local s_pkt = comms.scada_packet() @@ -108,7 +108,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() @@ -130,12 +130,12 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil unit_builds[unit.get_id()] = unit.get_build() end - _send(SCADA_CRDN_TYPE.INITIAL_BUILDS, { facility.get_build(), unit_builds }) + _send(CRDN_TYPE.INITIAL_BUILDS, { facility.get_build(), unit_builds }) end -- send facility builds local function _send_fac_builds() - _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build() }) + _send(CRDN_TYPE.FAC_BUILDS, { facility.get_build() }) end -- send unit builds @@ -147,7 +147,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil builds[unit.get_id()] = unit.get_build() end - _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds }) + _send(CRDN_TYPE.UNIT_BUILDS, { builds }) end -- send facility status @@ -158,7 +158,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil facility.get_alarm_tones() } - _send(SCADA_CRDN_TYPE.FAC_STATUS, status) + _send(CRDN_TYPE.FAC_STATUS, status) end -- send unit statuses @@ -178,7 +178,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil } end - _send(SCADA_CRDN_TYPE.UNIT_STATUSES, status) + _send(CRDN_TYPE.UNIT_STATUSES, status) end -- handle a packet @@ -200,7 +200,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil -- process packet if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then ---@cast pkt mgmt_frame - if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if pkt.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -219,7 +219,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then + elseif pkt.type == MGMT_TYPE.CLOSE then -- close the session _close() else @@ -227,22 +227,22 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil end elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then ---@cast pkt crdn_frame - if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then + if pkt.type == CRDN_TYPE.INITIAL_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.builds = true - elseif pkt.type == SCADA_CRDN_TYPE.FAC_BUILDS then + elseif pkt.type == CRDN_TYPE.FAC_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.fac_builds = true - elseif pkt.type == SCADA_CRDN_TYPE.FAC_CMD then + elseif pkt.type == CRDN_TYPE.FAC_CMD then if pkt.length >= 1 then local cmd = pkt.data[1] if cmd == FAC_COMMAND.SCRAM_ALL then facility.scram_all() - _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) + _send(CRDN_TYPE.FAC_CMD, { cmd, true }) elseif cmd == FAC_COMMAND.STOP then facility.auto_stop() - _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) + _send(CRDN_TYPE.FAC_CMD, { cmd, true }) elseif cmd == FAC_COMMAND.START then if pkt.length == 6 then ---@type coord_auto_config @@ -254,22 +254,22 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil limits = pkt.data[6] } - _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) + _send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) else log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch") end elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then facility.ack_all() - _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) + _send(CRDN_TYPE.FAC_CMD, { cmd, true }) elseif cmd == FAC_COMMAND.SET_WASTE_MODE then if pkt.length == 2 then - _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) }) + _send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) }) else log.debug(log_header .. "CRDN set waste mode packet length mismatch") end elseif cmd == FAC_COMMAND.SET_PU_FB then if pkt.length == 2 then - _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) }) + _send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) }) else log.debug(log_header .. "CRDN set pu fallback packet length mismatch") end @@ -279,10 +279,10 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil else log.debug(log_header .. "CRDN facility command packet length mismatch") end - elseif pkt.type == SCADA_CRDN_TYPE.UNIT_BUILDS then + elseif pkt.type == CRDN_TYPE.UNIT_BUILDS then -- acknowledgement to coordinator receiving builds self.acks.unit_builds = true - elseif pkt.type == SCADA_CRDN_TYPE.UNIT_CMD then + elseif pkt.type == CRDN_TYPE.UNIT_CMD then if pkt.length >= 2 then -- get command and unit id local cmd = pkt.data[1] @@ -315,7 +315,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil end elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then unit.ack_all() - _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, true }) + _send(CRDN_TYPE.UNIT_CMD, { cmd, uid, true }) elseif cmd == UNIT_COMMAND.ACK_ALARM then if pkt.length == 3 then unit.ack_alarm(pkt.data[3]) @@ -331,7 +331,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil elseif cmd == UNIT_COMMAND.SET_GROUP then if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then facility.set_group(unit.get_id(), pkt.data[3]) - _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] }) + _send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] }) else log.debug(log_header .. "CRDN unit command set group missing group id") end @@ -374,7 +374,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + _send_mgmt(MGMT_TYPE.CLOSE, {}) println("connection to coordinator " .. id .. " closed by server") log.info(log_header .. "session closed by server") end @@ -406,7 +406,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil if cmd.key == CRD_S_DATA.CMD_ACK then local ack = cmd.val ---@type coord_ack - _send(SCADA_CRDN_TYPE.UNIT_CMD, { ack.cmd, ack.unit, ack.ack }) + _send(CRDN_TYPE.UNIT_CMD, { ack.cmd, ack.unit, ack.ack }) elseif cmd.key == CRD_S_DATA.RESEND_PLC_BUILD then -- re-send PLC build -- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update @@ -419,7 +419,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil local unit = self.units[unit_id] ---@type reactor_unit builds[unit_id] = unit.get_build(-1) - _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds }) + _send(CRDN_TYPE.UNIT_BUILDS, { builds }) elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then local unit_id = cmd.val.unit if unit_id > 0 then @@ -433,14 +433,14 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil local unit = self.units[unit_id] ---@type reactor_unit builds[unit_id] = unit.get_build(cmd.val.type) - _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds }) + _send(CRDN_TYPE.UNIT_BUILDS, { builds }) else -- re-send facility RTU builds -- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update self.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD self.acks.fac_builds = false - _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) }) + _send(CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) }) end else log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true) @@ -474,7 +474,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) + _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 8fac94f..23fb1c6 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -12,7 +12,7 @@ local plc = {} local PROTOCOL = comms.PROTOCOL local RPLC_TYPE = comms.RPLC_TYPE -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE local PLC_AUTO_ACK = comms.PLC_AUTO_ACK local UNIT_COMMAND = comms.UNIT_COMMAND @@ -25,8 +25,9 @@ local PLC_S_CMDS = { SCRAM = 1, ASCRAM = 2, ENABLE = 3, - RPS_RESET = 4, - RPS_AUTO_RESET = 5 + DISABLE = 4, + RPS_RESET = 5, + RPS_AUTO_RESET = 6 } local PLC_S_DATA = { @@ -80,6 +81,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f retry_times = { struct_req = (util.time() + 500), status_req = (util.time() + 500), + disable_req = 0, scram_req = 0, ascram_req = 0, burn_rate_req = 0, @@ -87,6 +89,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f }, -- command acknowledgements acks = { + disable = true, scram = true, ascram = true, burn_rate = true, @@ -258,7 +261,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() @@ -482,7 +485,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f end elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then ---@cast pkt mgmt_frame - if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if pkt.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -501,7 +504,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then + elseif pkt.type == MGMT_TYPE.CLOSE then -- close the session _close() else @@ -595,7 +598,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + _send_mgmt(MGMT_TYPE.CLOSE, {}) println("connection to reactor " .. reactor_id .. " PLC closed by server") log.info(log_header .. "session closed by server") end @@ -627,6 +630,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f if not self.auto_lock then _send(RPLC_TYPE.RPS_ENABLE, {}) end + elseif cmd == PLC_S_CMDS.DISABLE then + -- disable the reactor + self.acks.disable = false + self.retry_times.disable_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPE.RPS_DISABLE, {}) elseif cmd == PLC_S_CMDS.SCRAM then -- SCRAM reactor self.acks.scram = false @@ -726,7 +734,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) + _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end @@ -780,6 +788,15 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f end end + -- reactor disable request retry + + if not self.acks.disable then + if rtimes.disable_req - util.time() <= 0 then + _send(RPLC_TYPE.RPS_DISABLE, {}) + rtimes.disable_req = util.time() + RETRY_PERIOD + end + end + -- SCRAM request retry if not self.acks.scram then diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua index 30ca7eb..48756ea 100644 --- a/supervisor/session/pocket.lua +++ b/supervisor/session/pocket.lua @@ -7,7 +7,7 @@ local databus = require("supervisor.databus") local pocket = {} local PROTOCOL = comms.PROTOCOL -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE -- retry time constants in ms -- local INITIAL_WAIT = 1500 @@ -76,7 +76,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() @@ -108,7 +108,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, -- process packet if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then ---@cast pkt mgmt_frame - if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if pkt.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -127,13 +127,13 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then + elseif pkt.type == MGMT_TYPE.CLOSE then -- close the session _close() - elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then + elseif pkt.type == MGMT_TYPE.DIAG_TONE_GET then -- get the state of alarm tones - _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones()) - elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then + _send_mgmt(MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones()) + elseif pkt.type == MGMT_TYPE.DIAG_TONE_SET then local valid = false -- attempt to set a tone state @@ -144,7 +144,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, -- try to set tone states, then send back if testing is allowed local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2]) - _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states }) + _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states }) else log.debug(log_header .. "SCADA diag tone set packet data type mismatch") end @@ -155,8 +155,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security") end - if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { false }) end - elseif pkt.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then + if not valid then _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { false }) end + elseif pkt.type == MGMT_TYPE.DIAG_ALARM_SET then local valid = false -- attempt to set an alarm state @@ -167,7 +167,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, -- try to set alarm states, then send back if testing is allowed local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2]) - _send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states }) + _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states }) else log.debug(log_header .. "SCADA diag alarm set packet data type mismatch") end @@ -178,7 +178,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security") end - if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { false }) end + if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end @@ -204,7 +204,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + _send_mgmt(MGMT_TYPE.CLOSE, {}) println("connection to pocket diag session " .. id .. " closed by server") log.info(log_header .. "session closed by server") end @@ -261,7 +261,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, facility, periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) + _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 1e42f49..41c3373 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -22,7 +22,7 @@ local svrs_turbinev = require("supervisor.session.rtu.turbinev") local rtu = {} local PROTOCOL = comms.PROTOCOL -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local PERIODICS = { @@ -223,7 +223,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement end -- send a SCADA management packet - ---@param msg_type SCADA_MGMT_TYPE + ---@param msg_type MGMT_TYPE ---@param msg table local function _send_mgmt(msg_type, msg) local s_pkt = comms.scada_packet() @@ -262,7 +262,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then ---@cast pkt mgmt_frame -- handle management packet - if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then + if pkt.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then local srv_start = pkt.data[1] @@ -281,17 +281,17 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement else log.debug(log_header .. "SCADA keep alive packet length mismatch") end - elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then + elseif pkt.type == MGMT_TYPE.CLOSE then -- close the session _close() - elseif pkt.type == SCADA_MGMT_TYPE.RTU_ADVERT then + elseif pkt.type == MGMT_TYPE.RTU_ADVERT then -- RTU unit advertisement log.debug(log_header .. "received updated advertisement") self.advert = pkt.data -- handle advertisement; this will re-create all unit sub-sessions _handle_advertisement() - elseif pkt.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT then + elseif pkt.type == MGMT_TYPE.RTU_DEV_REMOUNT then if pkt.length == 1 then local unit_id = pkt.data[1] if self.units[unit_id] ~= nil then @@ -322,7 +322,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement -- close the connection function public.close() _close() - _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) + _send_mgmt(MGMT_TYPE.CLOSE, {}) println(log_header .. "connection to RTU closed by server") log.info(log_header .. "session closed by server") end @@ -387,7 +387,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() }) + _send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end @@ -395,7 +395,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement periodics.alarm_tones = periodics.alarm_tones + elapsed if periodics.alarm_tones >= PERIODICS.ALARM_TONES then - _send_mgmt(SCADA_MGMT_TYPE.RTU_TONE_ALARM, { facility.get_alarm_tones() }) + _send_mgmt(MGMT_TYPE.RTU_TONE_ALARM, { facility.get_alarm_tones() }) periodics.alarm_tones = 0 end diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua index 9766bab..25ab3ed 100644 --- a/supervisor/session/rtu/txnctrl.lua +++ b/supervisor/session/rtu/txnctrl.lua @@ -55,7 +55,7 @@ function txnctrl.new() -- mark a transaction as resolved to get its transaction type ---@nodiscard ---@param txn_id integer - ---@return integer txn_type + ---@return integer|nil txn_type function public.resolve(txn_id) local txn_type = nil diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 711fc75..3d27fa4 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -73,7 +73,13 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t if m_pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then if m_pkt.unit_id == unit_id then local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) - local txn_tag = " (" .. util.strval(txn_tags[txn_type]) .. ")" + local txn_tag = util.c(" (", txn_tags[txn_type], ")") + + if txn_type == nil then + -- couldn't find this transaction + log.debug(log_tag .. "MODBUS: expired or spurious transaction reply (txn_id " .. m_pkt.txn_id .. ")") + return false, m_pkt.txn_id + end if bit.band(m_pkt.func_code, MODBUS_FCODE.ERROR_FLAG) ~= 0 then -- transaction incomplete or failed diff --git a/supervisor/startup.lua b/supervisor/startup.lua index dbde074..76fd746 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.2" +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 @@ -214,7 +213,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 diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 37707a3..498a51d 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -11,7 +11,7 @@ local supervisor = {} local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK -local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE +local MGMT_TYPE = comms.MGMT_TYPE -- supervisory controller communications ---@nodiscard @@ -58,7 +58,7 @@ function supervisor.comms(_version, nic, fp_ok) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack, data }) + m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data }) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) nic.transmit(packet.remote_channel(), svr_channel, s_pkt) @@ -147,7 +147,7 @@ function supervisor.comms(_version, nic, fp_ok) if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session local last_ack = self.last_est_acks[src_addr] @@ -221,7 +221,7 @@ function supervisor.comms(_version, nic, fp_ok) if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session local last_ack = self.last_est_acks[src_addr] @@ -275,7 +275,7 @@ function supervisor.comms(_version, nic, fp_ok) if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session local last_ack = self.last_est_acks[src_addr] @@ -291,7 +291,7 @@ function supervisor.comms(_version, nic, fp_ok) end _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) - elseif dev_type == DEVICE_TYPE.CRDN then + elseif dev_type == DEVICE_TYPE.CRD then -- this is an attempt to establish a new coordinator session local s_id = svsessions.establish_crd_session(src_addr, firmware_v) @@ -342,7 +342,7 @@ function supervisor.comms(_version, nic, fp_ok) if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session local last_ack = self.last_est_acks[src_addr] diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 262de6b..e0fe65e 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -645,6 +645,13 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- OPERATIONS -- --#region + -- queue a command to disable the reactor + function public.disable() + if self.plc_s ~= nil then + self.plc_s.in_queue.push_command(PLC_S_CMDS.DISABLE) + end + end + -- queue a command to SCRAM the reactor function public.scram() if self.plc_s ~= nil then diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index f2ee7aa..eb7fa27 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -358,6 +358,7 @@ end ---@param self _unit_self unit instance ---@param tripped boolean if the alarm condition is still active ---@param alarm alarm_def alarm table +---@return boolean new_trip if the alarm just changed to being tripped local function _update_alarm_state(self, tripped, alarm) local AISTATE = self.types.AISTATE local int_state = alarm.state @@ -439,7 +440,8 @@ local function _update_alarm_state(self, tripped, alarm) if alarm.state ~= int_state then local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state]) log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str)) - end + return alarm.state == AISTATE.TRIPPED + else return false end end -- evaluate alarm conditions @@ -469,11 +471,17 @@ function logic.update_alarms(self) -- Reactor Damage local rps_dmg_90 = plc_cache.rps_status.high_dmg and not self.last_rps_trips.high_dmg - _update_alarm_state(self, (plc_cache.damage > 0) or rps_dmg_90, self.alarms.ReactorDamage) + if _update_alarm_state(self, (plc_cache.damage > 0) or rps_dmg_90, self.alarms.ReactorDamage) then + log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorDamage.id]," <<")) + log.debug(util.c("| plc_cache.damage[", plc_cache.damage, "] rps_dmg_90[", rps_dmg_90, "]")) + end -- Over-Temperature local rps_high_temp = plc_cache.rps_status.high_temp and not self.last_rps_trips.high_temp - _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) + if _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp) then + log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorOverTemp.id]," <<")) + log.debug(util.c("| plc_cache.temp[", plc_cache.temp, "] rps_high_temp[", rps_high_temp, "]")) + end -- High Temperature _update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp) @@ -483,7 +491,10 @@ function logic.update_alarms(self) -- High Waste local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste - _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) + if _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste) then + log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.ReactorHighWaste.id]," <<")) + log.debug(util.c("| plc_cache.waste[", plc_cache.waste, "] rps_high_waste[", rps_high_waste, "]")) + end -- RPS Transient (excludes timeouts and manual trips) local rps_alarm = false @@ -514,7 +525,13 @@ function logic.update_alarms(self) rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch end - _update_alarm_state(self, rcs_trans, self.alarms.RCSTransient) + if _update_alarm_state(self, rcs_trans, self.alarms.RCSTransient) then + log.debug(util.c(">> Trip Detail Report for ", types.ALARM_NAMES[self.alarms.RCSTransient.id]," <<")) + log.debug(util.c("| any_low[", any_low, "] any_over[", any_over, "] gen_trip[", gen_trip, "]")) + log.debug(util.c("| RCPTrip[", annunc.RCPTrip, "] MaxWaterReturnFeed[", annunc.MaxWaterReturnFeed, "]")) + log.debug(util.c("| RCSFlowLow[", annunc.RCSFlowLow, "] BoilRateMismatch[", annunc.BoilRateMismatch, + "] CoolantFeedMismatch[", annunc.CoolantFeedMismatch, "] SteamFeedMismatch[", annunc.SteamFeedMismatch, "]")) + end -- Turbine Trip local any_trip = false @@ -522,9 +539,7 @@ function logic.update_alarms(self) _update_alarm_state(self, any_trip, self.alarms.TurbineTrip) -- update last trips table - for key, val in pairs(plc_cache.rps_status) do - self.last_rps_trips[key] = val - end + for key, val in pairs(plc_cache.rps_status) do self.last_rps_trips[key] = val end end -- update the internal automatic safety control performed while in auto control mode