diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6a9136a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: mikayla_f diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 8524d55..975bb73 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -5,12 +5,10 @@ on: push: branches: - main - - latest - devel pull_request: branches: - main - - latest - devel jobs: check: diff --git a/.github/workflows/manifest.yml b/.github/workflows/manifest.yml index a631058..efafe3d 100644 --- a/.github/workflows/manifest.yml +++ b/.github/workflows/manifest.yml @@ -6,7 +6,6 @@ on: push: branches: - main - - latest - devel # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages @@ -43,7 +42,7 @@ jobs: - name: Create outputs folders if: success() || failure() shell: bash - run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel + run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel - name: Generate manifest and shields for main branch id: manifest-main if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }} @@ -51,21 +50,6 @@ jobs: - name: Save main's manifest if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }} run: mv install_manifest.json deploy/manifests/main - # Generate manifest for latest branch - - name: Checkout latest - id: checkout-latest - if: success() || failure() - uses: actions/checkout@v3 - with: - ref: 'latest' - clean: false - - name: Generate manifest for latest - id: manifest-latest - if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }} - run: python imgen.py - - name: Save latest's manifest - if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }} - run: mv install_manifest.json deploy/manifests/latest # Generate manifest for devel branch - name: Checkout devel id: checkout-devel diff --git a/LICENSE b/LICENSE index e6824b5..7578961 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright © 2022 - 2023 Mikayla Fischler +Copyright © 2022 - 2024 Mikayla Fischler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/ccmsi.lua b/ccmsi.lua index 9b9a98c..cc4a5d0 100644 --- a/ccmsi.lua +++ b/ccmsi.lua @@ -1,7 +1,7 @@ --[[ CC-MEK-SCADA Installer Utility -Copyright (c) 2023 Mikayla Fischler +Copyright (c) 2023 - 2024 Mikayla Fischler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, @@ -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.12a" +local CCMSI_VERSION = "v1.14" local install_dir = "/.install-cache" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" @@ -26,7 +26,7 @@ local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada local opts = { ... } local mode, app, target -local install_manifest = manifest_path .. "main/install_manifest.json" +local install_manifest = manifest_path.."main/install_manifest.json" local function red() term.setTextColor(colors.red) end local function orange() term.setTextColor(colors.orange) end @@ -59,17 +59,17 @@ local function ask_y_n(question, default) end -- print out a white + blue text message -local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end +local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end -- indicate actions to be taken based on package differences for installs/updates local function show_pkg_change(name, v) if v.v_local ~= nil then if v.v_local ~= v.v_remote then - print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white() + print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white() elseif mode == "install" then - pkg_message("[" .. name .. "] reinstalling", v.v_local) + pkg_message("["..name.."] reinstalling", v.v_local) end - else pkg_message("[" .. name .. "] new install of", v.v_remote) end + else pkg_message("["..name.."] new install of", v.v_remote) end return v.v_local ~= v.v_remote end @@ -90,7 +90,7 @@ local function get_remote_manifest() local response, error = http.get(install_manifest) if response == nil then orange();println("Failed to get installation manifest from GitHub, cannot update or install.") - red();println("HTTP error: " .. error);white() + red();println("HTTP error: "..error);white() return false, {} end @@ -155,13 +155,13 @@ local function _clean_dir(dir, tree) if tree == nil then tree = {} end local ls = fs.list(dir) for _, val in pairs(ls) do - local path = dir .. "/" .. val + local path = dir.."/"..val 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)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files + if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end + elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then fs.delete(path) - println("deleted " .. path) + println("deleted "..path) end end end @@ -177,13 +177,13 @@ local function clean(manifest) local ls = fs.list("/") for _, val in pairs(ls) do if fs.isDriveRoot(val) then - yellow();println("skipped mount '" .. val .. "'") + yellow();println("skipped mount '"..val.."'") elseif fs.isDir(val) then - if tree[val] ~= nil then lgray();_clean_dir("/" .. val, tree[val]) - else white(); if ask_y_n("delete the unused directory '" .. val .. "'") then lgray();_clean_dir("/" .. val) end end - if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '" .. val .. "'") end + if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val]) + else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end + if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '"..val.."'") end elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then - white();if ask_y_n("delete the unused file '" .. val .. "'") then fs.delete(val);lgray();println("deleted " .. val) end + white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end end end @@ -192,7 +192,7 @@ end -- get and validate command line options -println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --") +println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") if #opts == 0 or opts[1] == "help" then println("usage: ccmsi ") @@ -202,8 +202,8 @@ if #opts == 0 or opts[1] == "help" then yellow() println(" ccmsi check for target") lgray() - println(" install - fresh install, overwrites config.lua") - println(" update - update files EXCEPT for config.lua") + println(" install - fresh install") + println(" update - update files") println(" uninstall - delete files INCLUDING config/logs") white();println("");lgray() println(" reactor-plc - reactor PLC firmware") @@ -213,7 +213,7 @@ if #opts == 0 or opts[1] == "help" then println(" pocket - pocket application") println(" installer - ccmsi installer (update only)") white();println("") - lgray();println(" main (default) | latest | devel");white() + lgray();println(" main (default) | devel");white() return else mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) @@ -233,14 +233,14 @@ else -- determine target if mode == "check" then target = opts[2] else target = opts[3] end - if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then + if (target ~= "main") and (target ~= "devel") then if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end target = "main" end -- set paths - install_manifest = manifest_path .. target .. "/install_manifest.json" - repo_path = repo_path .. target .. "/" + install_manifest = manifest_path..target.."/install_manifest.json" + repo_path = repo_path..target.."/" end -- run selected mode @@ -260,7 +260,7 @@ if mode == "check" then -- list all versions for key, value in pairs(manifest.versions) do term.setTextColor(colors.purple) - print(string.format("%-14s", "[" .. key .. "]")) + print(string.format("%-14s", "["..key.."]")) if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then blue();print(local_manifest.versions[key]) if value ~= local_manifest.versions[key] then @@ -315,10 +315,10 @@ elseif mode == "install" or mode == "update" then if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end if update_installer or ask_y_n("Would you like to update now") then lgray();println("GET ccmsi.lua") - local dl, err = http.get(repo_path .. "ccmsi.lua") + local dl, err = http.get(repo_path.."ccmsi.lua") if dl == nil then - red();println("HTTP Error " .. err) + red();println("HTTP Error "..err) println("Installer download failed.");white() else local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location @@ -342,13 +342,8 @@ elseif mode == "install" or mode == "update" then ver.lockbox.v_remote = manifest.versions.lockbox green() - if mode == "install" then - println("Installing " .. app .. " files...") - elseif mode == "update" then - if app == "coordinator" or app == "pocket" then println("Updating " .. app .. " files... (keeping old config.lua)") - else println("Updating " .. app .. " files...") end - end - white() + if mode == "install" then print("Installing ") else print("Updating ") end + println(app.." files...");white() ver.boot.changed = show_pkg_change("bootldr", ver.boot) ver.common.changed = show_pkg_change("common", ver.common) @@ -374,7 +369,6 @@ elseif mode == "install" or mode == "update" then local file_list = manifest.files local size_list = manifest.sizes local dependencies = manifest.depends[app] - local config_file = app .. "/config.lua" table.insert(dependencies, app) @@ -421,15 +415,15 @@ elseif mode == "install" or mode == "update" then local files = file_list[dependency] for _, file in pairs(files) do - println("GET " .. file) - local dl, err = http.get(repo_path .. file) + println("GET "..file) + local dl, err = http.get(repo_path..file) if dl == nil then - red();println("HTTP Error " .. err) + red();println("HTTP Error "..err) success = false break else - local handle = fs.open(install_dir .. "/" .. file, "w") + local handle = fs.open(install_dir.."/"..file, "w") handle.write(dl.readAll()) handle.close() end @@ -448,11 +442,9 @@ elseif mode == "install" or mode == "update" then local files = file_list[dependency] for _, file in pairs(files) do - if mode == "install" or file ~= config_file then - local temp_file = install_dir .. "/" .. file - if fs.exists(file) then fs.delete(file) end - fs.move(temp_file, file) - end + local temp_file = install_dir.."/"..file + if fs.exists(file) then fs.delete(file) end + fs.move(temp_file, file) end end end @@ -485,19 +477,17 @@ elseif mode == "install" or mode == "update" then local files = file_list[dependency] for _, file in pairs(files) do - if mode == "install" or file ~= config_file then - println("GET " .. file) - local dl, err = http.get(repo_path .. file) + println("GET "..file) + local dl, err = http.get(repo_path..file) - if dl == nil then - red();println("HTTP Error " .. err) - success = false - break - else - local handle = fs.open("/" .. file, "w") - handle.write(dl.readAll()) - handle.close() - end + if dl == nil then + red();println("HTTP Error "..err) + success = false + break + else + local handle = fs.open("/"..file, "w") + handle.write(dl.readAll()) + handle.close() end end end @@ -527,11 +517,11 @@ elseif mode == "uninstall" then end if manifest.versions[app] == nil then - red();println("Error: '" .. app .. "' is not installed.") + red();println("Error: '"..app.."' is not installed.") return end - orange();println("Uninstalling all " .. app .. " files...") + orange();println("Uninstalling all "..app.." files...") -- ask for confirmation if not ask_y_n("Continue", false) then return end @@ -546,16 +536,16 @@ elseif mode == "uninstall" then -- delete log file local log_deleted = false - local settings_file = app .. ".settings" - local legacy_config_file = app .. "/config.lua" + local settings_file = app..".settings" + local legacy_config_file = app.."/config.lua" lgray() if fs.exists(legacy_config_file) then log_deleted = pcall(function () - local config = require(app .. ".config") + local config = require(app..".config") if fs.exists(config.LOG_PATH) then fs.delete(config.LOG_PATH) - println("deleted log file " .. config.LOG_PATH) + println("deleted log file "..config.LOG_PATH) end end) elseif fs.exists(settings_file) and settings.load(settings_file) then @@ -563,7 +553,7 @@ elseif mode == "uninstall" then if log ~= nil and fs.exists(log) then log_deleted = true fs.delete(log) - println("deleted log file " .. log) + println("deleted log file "..log) end end @@ -577,7 +567,7 @@ elseif mode == "uninstall" then for _, dependency in pairs(dependencies) do local files = file_list[dependency] for _, file in pairs(files) do - if fs.exists(file) then fs.delete(file);println("deleted " .. file) end + if fs.exists(file) then fs.delete(file);println("deleted "..file) end end local folder = files[1] @@ -588,13 +578,16 @@ elseif mode == "uninstall" then if fs.isDir(folder) then fs.delete(folder) - println("deleted directory " .. folder) + println("deleted directory "..folder) end end + if fs.exists(legacy_config_file) then + fs.delete(legacy_config_file);println("deleted "..legacy_config_file) + end + if fs.exists(settings_file) then - fs.delete(settings_file) - println("deleted " .. settings_file) + fs.delete(settings_file);println("deleted "..settings_file) end fs.delete("install_manifest.json") diff --git a/configure.lua b/configure.lua index ab0b64e..ce6ed40 100644 --- a/configure.lua +++ b/configure.lua @@ -1,15 +1,10 @@ print("CONFIGURE> SCANNING FOR CONFIGURATOR...") -if fs.exists("reactor-plc/configure.lua") then - require("reactor-plc.configure").configure() -elseif fs.exists("rtu/configure.lua") then - require("rtu.configure").configure() -elseif fs.exists("supervisor/configure.lua") then - require("supervisor.configure").configure() -elseif fs.exists("coordinator/startup.lua") then - print("CONFIGURE> coordinator configurator not yet implemented (use 'edit coordinator/config.lua' to configure)") -elseif fs.exists("pocket/startup.lua") then - print("CONFIGURE> pocket configurator not yet implemented (use 'edit pocket/config.lua' to configure)") +if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure() +elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure() +elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure() +elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure() +elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure() else print("CONFIGURE> NO CONFIGURATOR FOUND") print("CONFIGURE> EXIT") diff --git a/coordinator/config.lua b/coordinator/config.lua deleted file mode 100644 index bdf01e2..0000000 --- a/coordinator/config.lua +++ /dev/null @@ -1,41 +0,0 @@ -local config = {} - --- supervisor comms channel -config.SVR_CHANNEL = 16240 --- coordinator comms channel -config.CRD_CHANNEL = 16243 --- pocket comms channel -config.PKT_CHANNEL = 16244 --- 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.SV_TIMEOUT = 5 -config.API_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" - --- expected number of reactor units, used only to require that number of unit monitors -config.NUM_UNITS = 4 - --- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play()) --- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale -config.SOUNDER_VOLUME = 1.0 - --- true for 24 hour time on main view screen -config.TIME_24_HOUR = true - --- disable flow view (for legacy layouts) -config.DISABLE_FLOW_VIEW = false - --- 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/coordinator/configure.lua b/coordinator/configure.lua new file mode 100644 index 0000000..7abb267 --- /dev/null +++ b/coordinator/configure.lua @@ -0,0 +1,1312 @@ +-- +-- Configuration GUI +-- + +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local network = require("scada-common.network") +local ppm = require("scada-common.ppm") +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 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 tri = util.trinary + +local PROTOCOL = comms.PROTOCOL +local DEVICE_TYPE = comms.DEVICE_TYPE +local ESTABLISH_ACK = comms.ESTABLISH_ACK +local MGMT_TYPE = comms.MGMT_TYPE + +local cpair = core.cpair + +local LEFT = core.ALIGN.LEFT +local CENTER = core.ALIGN.CENTER +local RIGHT = core.ALIGN.RIGHT + +-- changes to the config data/format to let the user know +local changes = {} + +---@class crd_configurator +local configurator = {} + +local style = {} + +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) + +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 bw_fg_bg = cpair(colors.black, colors.white) +local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) +local nav_fg_bg = bw_fg_bg +local btn_act_fg_bg = cpair(colors.white, colors.gray) +local dis_fg_bg = cpair(colors.lightGray,colors.white) + +local tool_ctl = { + nic = nil, ---@type nic + net_listen = false, + sv_addr = comms.BROADCAST, + sv_seq_num = 0, + sv_cool_conf = nil, ---@type table list of boiler & turbine counts + show_sv_cfg = nil, ---@type function + + start_fail = 0, + fail_message = "", + has_config = false, + viewing_config = false, + importing_legacy = false, + + view_cfg = nil, ---@type graphics_element + settings_apply = nil, ---@type graphics_element + + 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 = "", + + sv_connect = nil, ---@type function + sv_conn_button = nil, ---@type graphics_element + sv_conn_status = nil, ---@type graphics_element + sv_conn_detail = nil, ---@type graphics_element + sv_skip = nil, ---@type graphics_element + sv_next = nil, ---@type graphics_element + + apply_mon = nil, ---@type graphics_element + + update_mon_reqs = nil, ---@type function + gen_mon_list = function () end, + edit_monitor = nil, ---@type function + + mon_iface = "", + mon_expect = {} +} + +---@class crd_config +local tmp_cfg = { + UnitCount = 1, + SpeakerVolume = 1.0, + Time24Hour = true, + DisableFlowView = false, + MainDisplay = nil, ---@type string + FlowDisplay = nil, ---@type string + UnitDisplays = {}, + SVR_Channel = nil, ---@type integer + CRD_Channel = nil, ---@type integer + PKT_Channel = nil, ---@type integer + SVR_Timeout = nil, ---@type number + API_Timeout = nil, ---@type number + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string|nil + LogMode = 0, + LogPath = "", + LogDebug = false, +} + +---@class crd_config +local ini_cfg = {} +---@class crd_config +local settings_cfg = {} + +-- all settings fields, their nice names, and their default values +local fields = { + { "UnitCount", "Number of Reactors", 1 }, + { "MainDisplay", "Main Monitor", nil }, + { "FlowDisplay", "Flow Monitor", nil }, + { "UnitDisplays", "Unit Monitors", {} }, + { "SpeakerVolume", "Speaker Volume", 1.0 }, + { "Time24Hour", "Use 24-hour Time Format", true }, + { "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false }, + { "SVR_Channel", "SVR Channel", 16240 }, + { "CRD_Channel", "CRD Channel", 16243 }, + { "PKT_Channel", "PKT Channel", 16244 }, + { "SVR_Timeout", "Supervisor Connection Timeout", 5 }, + { "API_Timeout", "API Connection Timeout", 5 }, + { "TrustedRange", "Trusted Range", 0 }, + { "AuthKey", "Facility Auth Key" , ""}, + { "LogMode", "Log Mode", log.MODE.APPEND }, + { "LogPath", "Log Path", "/log.txt" }, + { "LogDebug","Log Debug Messages", false } +} + +-- check if a value is an integer within a range (inclusive) +---@param x integer +---@param min integer +---@param max integer +local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end + +-- send a management packet to the supervisor +---@param msg_type MGMT_TYPE +---@param msg table +local function send_sv(msg_type, msg) + local s_pkt = comms.scada_packet() + local pkt = comms.mgmt_packet() + + pkt.make(msg_type, msg) + s_pkt.make(tool_ctl.sv_addr, tool_ctl.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + + tool_ctl.nic.transmit(tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, s_pkt) + tool_ctl.sv_seq_num = tool_ctl.sv_seq_num + 1 +end + +-- handle an establish message from the supervisor +---@param packet mgmt_frame +local function handle_packet(packet) + local error_msg = nil + + if packet.scada_frame.local_channel() ~= tmp_cfg.CRD_Channel then + error_msg = "Error: unknown receive channel." + elseif packet.scada_frame.remote_channel() == tmp_cfg.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then + if packet.type == MGMT_TYPE.ESTABLISH then + if packet.length == 2 then + local est_ack = packet.data[1] + local config = packet.data[2] + + if est_ack == ESTABLISH_ACK.ALLOW then + if type(config) == "table" and #config == 2 then + local count_ok = is_int_min_max(config[1], 1, 4) + local cool_ok = type(config[2]) == "table" and type(config[2].r_cool) == "table" and #config[2].r_cool == config[1] + + if count_ok and cool_ok then + tmp_cfg.UnitCount = config[1] + tool_ctl.sv_cool_conf = {} + + for i = 1, tmp_cfg.UnitCount do + local num_b = config[2].r_cool[i].BoilerCount + local num_t = config[2].r_cool[i].TurbineCount + tool_ctl.sv_cool_conf[i] = { num_b, num_t } + cool_ok = cool_ok and is_int_min_max(num_b, 0, 2) and is_int_min_max(num_t, 1, 3) + end + end + + if not count_ok then + error_msg = "Error: supervisor unit count out of range." + elseif not cool_ok then + error_msg = "Error: supervisor cooling configuration malformed." + tool_ctl.sv_cool_conf = nil + end + + tool_ctl.sv_addr = packet.scada_frame.src_addr() + send_sv(MGMT_TYPE.CLOSE, {}) + else + error_msg = "Error: invalid cooling configuration supervisor." + end + else + error_msg = "Error: invalid allow reply length from supervisor." + end + elseif packet.length == 1 then + local est_ack = packet.data[1] + + if est_ack == ESTABLISH_ACK.DENY then + error_msg = "Error: supervisor connection denied." + elseif est_ack == ESTABLISH_ACK.COLLISION then + error_msg = "Error: a coordinator is already/still connected. Please try again." + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + error_msg = "Error: coordinator comms version does not match supervisor comms version." + else + error_msg = "Error: invalid reply from supervisor." + end + else + error_msg = "Error: invalid reply length from supervisor." + end + else + error_msg = "Error: didn't get an establish reply from supervisor." + end + end + + tool_ctl.net_listen = false + + if error_msg then + tool_ctl.sv_conn_status.set_value("") + tool_ctl.sv_conn_detail.set_value(error_msg) + tool_ctl.sv_conn_button.enable() + else + tool_ctl.sv_conn_status.set_value("Connected!") + tool_ctl.sv_conn_detail.set_value("Data received successfully, press 'Next' to continue.") + tool_ctl.sv_skip.hide() + tool_ctl.sv_next.show() + end +end + +-- handle supervisor connection failure +local function handle_timeout() + tool_ctl.net_listen = false + tool_ctl.sv_conn_button.enable() + tool_ctl.sv_conn_status.set_value("Timed out.") + tool_ctl.sv_conn_detail.set_value("Supervisor did not reply. Ensure startup app is running on the supervisor.") +end + +-- load tmp_cfg fields from ini_cfg fields for displays +local function preset_monitor_fields() + tmp_cfg.DisableFlowView = ini_cfg.DisableFlowView + + tmp_cfg.MainDisplay = ini_cfg.MainDisplay + tmp_cfg.FlowDisplay = ini_cfg.FlowDisplay + for i = 1, ini_cfg.UnitCount do + tmp_cfg.UnitDisplays[i] = ini_cfg.UnitDisplays[i] + end +end + +-- load data from the settings file +---@param target crd_config +---@param raw boolean? true to not use default values +local function load_settings(target, raw) + for _, v in pairs(fields) do settings.unset(v[1]) end + + local loaded = settings.load("/coordinator.settings") + + for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end + + return loaded +end + +-- create the config view +---@param display graphics_element +local function config_view(display) +---@diagnostic disable-next-line: undefined-field + local function exit() os.queueEvent("terminate") end + + TextBox{parent=display,y=1,text="Coordinator 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 net_cfg = Div{parent=root_pane_div,x=1,y=1} + local fac_cfg = Div{parent=root_pane_div,x=1,y=1} + local mon_cfg = Div{parent=root_pane_div,x=1,y=1} + local spkr_cfg = Div{parent=root_pane_div,x=1,y=1} + local crd_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 changelog = Div{parent=root_pane_div,x=1,y=1} + + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,fac_cfg,mon_cfg,spkr_cfg,crd_cfg,log_cfg,summary,changelog}} + + -- Main Page + + local y_start = 5 + + TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Coordinator configurator! Please select one of the following options."} + + if tool_ctl.start_fail == 2 then + local msg = util.c("Notice: There is a problem with your monitor configuration. ", tool_ctl.fail_message, " Please reconfigure monitors or correct their sizes.") + TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text=msg,fg_bg=cpair(colors.red,colors.lightGray)} + y_start = y_start + 5 + elseif tool_ctl.start_fail > 0 then + TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)} + y_start = y_start + 5 + end + + local function view_config() + tool_ctl.viewing_config = true + tool_ctl.gen_summary(settings_cfg) + tool_ctl.settings_apply.hide(true) + main_pane.set_value(8) + end + + if fs.exists("/coordinator/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=dis_fg_bg} + + 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} + PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(9)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#region Network + + 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_c_4 = 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,net_c_4}} + + TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,height=1,text="Please set the network channels below."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 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=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"} + local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"} + local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"} + local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} + + local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_channels() + local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) + if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then + tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c + net_pane.set_value(2) + chan_err.hide(true) + else chan_err.show() end + end + + PushButton{parent=net_c_1,x=1,y=14,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=net_c_1,x=44,y=14,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="Please set the connection timeouts below."} + TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_2,x=1,y=8,height=1,width=19,text="Supervisor Timeout"} + local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=1,y=10,height=1,width=14,text="Pocket Timeout"} + local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg} + + local ct_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_timeouts() + local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value()) + if svr_cto ~= nil and api_cto ~= nil then + tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto + net_pane.set_value(3) + ct_err.hide(true) + else ct_err.show() end + end + + PushButton{parent=net_c_2,x=1,y=14,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,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,height=1,text="Please set the trusted range below."} + TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} + + local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} + + local tr_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_tr() + local range_val = tonumber(range.get_value()) + if range_val ~= nil then + tmp_cfg.TrustedRange = range_val + comms.set_trusted_range(range_val) + net_pane.set_value(4) + tr_err.hide(true) + else tr_err.show() end + end + + PushButton{parent=net_c_3,x=1,y=14,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,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."} + TextBox{parent=net_c_4,x=1,y=4,height=6,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=g_lg_fg_bg} + + TextBox{parent=net_c_4,x=1,y=11,height=1,text="Facility Auth Key"} + local key, _, censor = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg} + + local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end + + local hide_key = CheckBox{parent=net_c_4,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 key_err = TextBox{parent=net_c_4,x=8,y=14,height=1,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_auth() + local v = key.get_value() + if string.len(v) == 0 or string.len(v) >= 8 then + -- prep supervisor connection screen + tool_ctl.sv_next.hide() + tool_ctl.sv_skip.disable() + tool_ctl.sv_skip.show() + tool_ctl.sv_conn_button.enable() + tool_ctl.sv_conn_status.set_value("") + tool_ctl.sv_conn_detail.set_value("") + + tmp_cfg.AuthKey = key.get_value() + key_err.hide(true) + + -- init mac for supervisor connection + if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) end + + main_pane.set_value(3) + + tcd.dispatch_unique(2, function () tool_ctl.sv_skip.enable() end) + else key_err.show() end + end + + PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Facility + + local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49} + local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49} + + local fac_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}} + + TextBox{parent=fac_cfg,x=1,y=2,height=1,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} + + TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."} + TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."} + + tool_ctl.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,height=1,text=""} + tool_ctl.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""} + + tool_ctl.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()tool_ctl.sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} + + local function sv_skip() + tcd.abort(handle_timeout) + tool_ctl.sv_cool_conf = nil + tool_ctl.net_listen = false + fac_pane.set_value(2) + end + + local function sv_next() + tool_ctl.show_sv_cfg() + tool_ctl.update_mon_reqs() + fac_pane.set_value(3) + end + + PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} + tool_ctl.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true} + + TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."} + local num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg} + TextBox{parent=fac_c_2,x=7,y=5,height=1,text="reactors"} + TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=g_lg_fg_bg} + TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg} + + local nu_error = TextBox{parent=fac_c_2,x=8,y=14,height=1,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_num_units() + local count = tonumber(num_units.get_value()) + if count ~= nil and count > 0 and count < 5 then + nu_error.hide(true) + tmp_cfg.UnitCount = count + tool_ctl.update_mon_reqs() + main_pane.set_value(4) + else nu_error.show() end + end + + PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."} + + local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Monitors + + local mon_c_1 = Div{parent=mon_cfg,x=2,y=4,width=49} + local mon_c_2 = Div{parent=mon_cfg,x=2,y=4,width=49} + local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49} + local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49} + + local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}} + + TextBox{parent=mon_cfg,x=1,y=2,height=1,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)} + + TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."} + local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function next_from_reqs() + -- unassign unit monitors above the unit count + for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end + + tool_ctl.gen_mon_list() + mon_pane.set_value(2) + end + + PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."} + + local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local assign_err = TextBox{parent=mon_c_2,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_monitors() + if tmp_cfg.MainDisplay == nil then + assign_err.set_value("Please assign the main monitor.") + elseif tmp_cfg.FlowDisplay == nil and not tmp_cfg.DisableFlowView then + assign_err.set_value("Please assign the flow monitor.") + elseif util.table_len(tmp_cfg.UnitDisplays) ~= tmp_cfg.UnitCount then + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == nil then + assign_err.set_value("Please assign the unit " .. i .. " monitor.") + break + end + end + else + assign_err.hide(true) + main_pane.set_value(5) + return + end + + assign_err.show() + end + + PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""} + + local mon_unit_l, mon_unit = nil, nil ---@type graphics_element, graphics_element + + local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)} + + ---@param val integer assignment type + local function on_assign_mon(val) + if val == 2 and tmp_cfg.DisableFlowView then + tool_ctl.apply_mon.disable() + mon_warn.set_value("You disabled having a flow view monitor. It can't be set unless you go back and enable it.") + mon_warn.show() + elseif not util.table_contains(tool_ctl.mon_expect, val) then + tool_ctl.apply_mon.disable() + mon_warn.set_value("That assignment doesn't fit monitor dimensions. You'll need to resize the monitor for it to work.") + mon_warn.show() + else + tool_ctl.apply_mon.enable() + mon_warn.hide(true) + end + + if val == 3 then + mon_unit_l.show() + mon_unit.show() + else + mon_unit_l.hide(true) + mon_unit.hide(true) + end + + local value = mon_unit.get_value() + mon_unit.set_max(tmp_cfg.UnitCount) + if value == "0" or value == nil then mon_unit.set_value(0) end + end + + TextBox{parent=mon_c_3,x=1,y=6,width=10,height=1,text="Assignment"} + local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue} + + mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,height=1,text="Unit ID"} + mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg} + + local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,height=1,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + -- purge all assignments for a given monitor + ---@param iface string + local function purge_assignments(iface) + if tmp_cfg.MainDisplay == iface then + tmp_cfg.MainDisplay = nil + elseif tmp_cfg.FlowDisplay == iface then + tmp_cfg.FlowDisplay = nil + else + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == iface then tmp_cfg.UnitDisplays[i] = nil end + end + end + end + + local function apply_monitor() + local iface = tool_ctl.mon_iface + local type = mon_assign.get_value() + local u_id = tonumber(mon_unit.get_value()) + + if type == 1 then + purge_assignments(iface) + tmp_cfg.MainDisplay = iface + elseif type == 2 then + purge_assignments(iface) + tmp_cfg.FlowDisplay = iface + elseif u_id and u_id > 0 then + purge_assignments(iface) + tmp_cfg.UnitDisplays[u_id] = iface + else + mon_u_err.show() + return + end + + tool_ctl.gen_mon_list() + mon_u_err.hide(true) + mon_pane.set_value(2) + end + + PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg} + + TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."} + TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."} + + local dis_flow_view = CheckBox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)} + + local function back_from_legacy() + tmp_cfg.DisableFlowView = dis_flow_view.get_value() + tool_ctl.update_mon_reqs() + mon_pane.set_value(1) + end + + PushButton{parent=mon_c_4,x=1,y=14,text="\x1b Back",callback=back_from_legacy,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Speaker + + local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} + + TextBox{parent=spkr_cfg,x=1,y=2,height=1,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)} + + TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."} + TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."} + + local s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg} + + TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg} + + local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,height=1,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_vol() + local vol = tonumber(s_vol.get_value()) + if vol ~= nil then + s_vol_err.hide(true) + tmp_cfg.SpeakerVolume = vol + main_pane.set_value(6) + else s_vol_err.show() end + end + + PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Coordinator UI + + local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49} + + local crd_pane = MultiPane{parent=crd_cfg,x=1,y=4,panes={crd_c_1}} + + TextBox{parent=crd_cfg,x=1,y=2,height=1,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)} + + TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."} + + TextBox{parent=crd_c_1,x=1,y=4,height=1,text="Clock Time Format"} + local clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} + + local function submit_ui_opts() + tmp_cfg.Time24Hour = clock_fmt.get_value() == 1 + main_pane.set_value(7) + end + + PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Logging + + 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=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,height=1,text="Please configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,height=1,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="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=bw_fg_bg} + + 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="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg} + + local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,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(8) + else path_err.show() end + end + + PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Summary and Saving + + 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=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function back_from_summary() + 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(7) + 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("/coordinator.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(crd_chan, ini_cfg.CRD_Channel) + try_set(pkt_chan, ini_cfg.PKT_Channel) + try_set(svr_timeout, ini_cfg.SVR_Timeout) + try_set(api_timeout, ini_cfg.API_Timeout) + try_set(range, ini_cfg.TrustedRange) + try_set(key, ini_cfg.AuthKey) + try_set(num_units, ini_cfg.UnitCount) + try_set(dis_flow_view, ini_cfg.DisableFlowView) + try_set(s_vol, ini_cfg.SpeakerVolume) + try_set(clock_fmt, util.trinary(ini_cfg.Time24Hour, 1, 2)) + try_set(mode, ini_cfg.LogMode) + try_set(path, ini_cfg.LogPath) + try_set(en_dbg, ini_cfg.LogDebug) + + preset_monitor_fields() + + tool_ctl.gen_mon_list() + + tool_ctl.view_cfg.enable() + + 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,text="\x1b Back",callback=back_from_summary,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=dis_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} + + TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"} + + local function go_home() + main_pane.set_value(1) + net_pane.set_value(1) + fac_pane.set_value(1) + mon_pane.set_value(1) + crd_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="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/coordinator/config.lua") + fs.delete("/coord.settings") + 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="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)} + + --#endregion + + -- Config Change Log + + local cl = Div{parent=changelog,x=2,y=4,width=49} + + TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} + + local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + for _, change in ipairs(changes) do + TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} + for _, v in ipairs(change[2]) do + local e = Div{parent=c_log,height=#util.strwrap(v,46)} + TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)} + end + end + + PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + -- set tool functions now that we have the elements + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("coordinator.config") + + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.CRD_Channel = config.CRD_CHANNEL + tmp_cfg.PKT_Channel = config.PKT_CHANNEL + tmp_cfg.SVR_Timeout = config.SV_TIMEOUT + tmp_cfg.API_Timeout = config.API_TIMEOUT + tmp_cfg.TrustedRange = config.TRUSTED_RANGE + tmp_cfg.AuthKey = config.AUTH_KEY or "" + + tmp_cfg.UnitCount = config.NUM_UNITS + tmp_cfg.DisableFlowView = config.DISABLE_FLOW_VIEW + tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME + tmp_cfg.Time24Hour = config.TIME_24_HOUR + + tmp_cfg.LogMode = config.LOG_MODE + tmp_cfg.LogPath = config.LOG_PATH + tmp_cfg.LogDebug = config.LOG_DEBUG or false + + settings.load("/coord.settings") + + tmp_cfg.MainDisplay = settings.get("PRIMARY_DISPLAY") + tmp_cfg.FlowDisplay = settings.get("FLOW_DISPLAY") + tmp_cfg.UnitDisplays = settings.get("UNIT_DISPLAYS", {}) + + -- if there are extra monitor entries, delete them now + -- not doing so will cause the app to fail to start + if is_int_min_max(tmp_cfg.UnitCount, 1, 4) then + for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end + end + + if settings.get("ControlStates") == nil then + local ctrl_states = { + process = settings.get("PROCESS"), + waste_modes = settings.get("WASTE_MODES"), + priority_groups = settings.get("PRIORITY_GROUPS"), + } + + settings.set("ControlStates", ctrl_states) + end + + settings.unset("PRIMARY_DISPLAY") + settings.unset("FLOW_DISPLAY") + settings.unset("UNIT_DISPLAYS") + settings.unset("PROCESS") + settings.unset("WASTE_MODES") + settings.unset("PRIORITY_GROUPS") + + tool_ctl.gen_summary(tmp_cfg) + sum_pane.set_value(1) + main_pane.set_value(8) + tool_ctl.importing_legacy = true + end + + -- attempt a connection to the supervisor to get cooling info + function tool_ctl.sv_connect() + tool_ctl.sv_conn_button.disable() + tool_ctl.sv_conn_detail.set_value("") + + local modem = ppm.get_wireless_modem() + if modem == nil then + tool_ctl.sv_conn_status.set_value("Please connect an ender/wireless modem.") + else + tool_ctl.sv_conn_status.set_value("Modem found, connecting...") + if tool_ctl.nic == nil then tool_ctl.nic = network.nic(modem) end + + tool_ctl.nic.closeAll() + tool_ctl.nic.open(tmp_cfg.CRD_Channel) + + tool_ctl.sv_addr = comms.BROADCAST + tool_ctl.sv_seq_num = 0 + tool_ctl.net_listen = true + + send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD }) + + tcd.dispatch_unique(8, handle_timeout) + end + end + + -- show the facility's unit count and cooling configuration data + function tool_ctl.show_sv_cfg() + local conf = tool_ctl.sv_cool_conf + fac_config_list.remove_all() + + local str = util.sprintf("Facility has %d reactor unit%s:", #conf, util.trinary(#conf==1,"","s")) + TextBox{parent=fac_config_list,height=1,text=str,fg_bg=cpair(colors.gray,colors.white)} + + for i = 1, #conf do + local num_b, num_t = conf[i][1], conf[i][2] + str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, util.trinary(num_b == 1, "", "s"), num_t, util.trinary(num_t == 1, "", "s")) + TextBox{parent=fac_config_list,height=1,text=str,fg_bg=cpair(colors.gray,colors.white)} + end + end + + -- update list of monitor requirements + function tool_ctl.update_mon_reqs() + local plural = tmp_cfg.UnitCount > 1 + + if tool_ctl.sv_cool_conf ~= nil then + local cnf = tool_ctl.sv_cool_conf + + local row1_tall = cnf[1][1] > 1 or cnf[1][2] > 2 or (cnf[2] and (cnf[2][1] > 1 or cnf[2][2] > 2)) + local row1_short = (cnf[1][1] == 0 and cnf[1][2] == 1) and (cnf[2] == nil or (cnf[2][1] == 0 and cnf[2][2] == 1)) + local row2_tall = (cnf[3] and (cnf[3][1] > 1 or cnf[3][2] > 2)) or (cnf[4] and (cnf[4][1] > 1 or cnf[4][2] > 2)) + local row2_short = (cnf[3] == nil or (cnf[3][1] == 0 and cnf[3][2] == 1)) and (cnf[4] == nil or (cnf[4][1] == 0 and cnf[4][2] == 1)) + + if tmp_cfg.UnitCount <= 2 then + tool_ctl.main_mon_h = util.trinary(row1_tall, 5, 4) + else + -- is only one tall and the other short, or are both tall? -> 5 or 6; are neither tall? -> 5 + if row1_tall or row2_tall then + tool_ctl.main_mon_h = util.trinary((row1_short and row2_tall) or (row1_tall and row2_short), 5, 6) + else tool_ctl.main_mon_h = 5 end + end + else + tool_ctl.main_mon_h = util.trinary(tmp_cfg.UnitCount <= 2, 4, 5) + end + + tool_ctl.flow_mon_h = 2 + tmp_cfg.UnitCount + + local asterisk = util.trinary(tool_ctl.sv_cool_conf == nil, "*", "") + local m_at_least = util.trinary(tool_ctl.main_mon_h < 6, "at least ", "") + local f_at_least = util.trinary(tool_ctl.flow_mon_h < 6, "at least ", "") + + mon_reqs.remove_all() + + TextBox{parent=mon_reqs,x=1,y=1,height=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")} + TextBox{parent=mon_reqs,x=1,y=1,height=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=mon_reqs,x=1,y=1,height=1,text="\x1a 1 Main View Monitor"} + TextBox{parent=mon_reqs,x=1,y=1,height=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)} + if not tmp_cfg.DisableFlowView then + TextBox{parent=mon_reqs,x=1,y=1,height=1,text="\x1a 1 Flow View Monitor"} + TextBox{parent=mon_reqs,x=1,y=1,height=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)} + end + end + + -- set/edit a monitor's assignment + ---@param iface string + ---@param device ppm_entry + function tool_ctl.edit_monitor(iface, device) + tool_ctl.mon_iface = iface + + local dev = device.dev + local w, h = ppm.monitor_block_size(dev.getSize()) + + local msg = "This size doesn't match a required screen. Please go back and resize it, or configure below at the risk of it not working." + + tool_ctl.mon_expect = {} + mon_assign.set_value(1) + mon_unit.set_value(0) + + if w == 4 and h == 4 then + msg = "This could work as a unit display. Please configure below." + tool_ctl.mon_expect = { 3 } + mon_assign.set_value(3) + elseif w == 8 then + if h >= tool_ctl.main_mon_h and h >= tool_ctl.flow_mon_h then + msg = "This could work as either your main monitor or flow monitor. Please configure below." + tool_ctl.mon_expect = { 1, 2 } + if tmp_cfg.MainDisplay then mon_assign.set_value(2) end + elseif h >= tool_ctl.main_mon_h then + msg = "This could work as your main monitor. Please configure below." + tool_ctl.mon_expect = { 1 } + elseif h >= tool_ctl.flow_mon_h then + msg = "This could work as your flow monitor. Please configure below." + tool_ctl.mon_expect = { 2 } + mon_assign.set_value(2) + end + end + + -- override if a config exists + if tmp_cfg.MainDisplay == iface then + mon_assign.set_value(1) + elseif tmp_cfg.FlowDisplay == iface then + mon_assign.set_value(2) + else + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == iface then + mon_assign.set_value(3) + mon_unit.set_value(i) + break + end + end + end + + on_assign_mon(mon_assign.get_value()) + + mon_desc.set_value(util.c("You have selected '", iface, "', which has a block size of ", w, " wide by ", h, " tall. ", msg)) + mon_pane.set_value(3) + end + + -- generate the list of available monitors + function tool_ctl.gen_mon_list() + mon_list.remove_all() + + local monitors = ppm.get_monitor_list() + for iface, device in pairs(monitors) do + local dev = device.dev + + dev.setTextScale(0.5) + dev.setTextColor(colors.white) + dev.setBackgroundColor(colors.black) + dev.clear() + dev.setCursorPos(1, 1) + dev.setTextColor(colors.magenta) + dev.write("This is monitor") + dev.setCursorPos(1, 2) + dev.setTextColor(colors.white) + dev.write(iface) + + local assignment = "Unused" + + if tmp_cfg.MainDisplay == iface then + assignment = "Main" + elseif tmp_cfg.FlowDisplay == iface then + assignment = "Flow" + else + for i = 1, tmp_cfg.UnitCount do + if tmp_cfg.UnitDisplays[i] == iface then + assignment = "Unit " .. i + break + end + end + end + + local line = Div{parent=mon_list,x=1,y=1,height=1} + + TextBox{parent=line,x=1,y=1,width=6,height=1,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)} + TextBox{parent=line,x=8,y=1,height=1,text=iface} + + local w, h = ppm.monitor_block_size(dev.getSize()) + + local function unset_mon() + purge_assignments(iface) + tool_ctl.gen_mon_list() + end + + TextBox{parent=line,x=33,y=1,width=4,height=1,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)} + PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()tool_ctl.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} + local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)} + + if assignment == "Unused" then unset.disable() end + end + 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 crd_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)) + elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") + elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then + val = "" + for idx = 1, #cfg.UnitDisplays do + val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 Unit %d - %s", idx, cfg.UnitDisplays[idx]) + end + end + + if val == "nil" then val = "" end + + local c = util.trinary(alternate, g_lg_fg_bg, 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 + + if (f[1] == "UnitDisplays") and (height == 1) and (val ~= "") then height = 2 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 coordinator configurator
+-- start_fail of 0 is OK (default if not provided), 1 is bad config, 2 is bad monitor config +---@param start_code? 0|1|2 indicate error state when called from the startup app +---@param message? any string message to display on a start_fail of 2 +function configurator.configure(start_code, message) + tool_ctl.start_fail = start_code or 0 + tool_ctl.fail_message = util.trinary(type(message) == "string", message, "") + + load_settings(settings_cfg, true) + tool_ctl.has_config = load_settings(ini_cfg) + + -- copy in some important values to start with + preset_monitor_fields() + + reset_term() + + ppm.mount_all() + + -- 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) + + tool_ctl.gen_mon_list() + + while true do + local event, param1, param2, param3, param4, param5 = util.pull_event() + + -- handle event + if event == "timer" then + tcd.handle(param1) + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then + 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 + 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 + display.handle_paste(param1) + elseif event == "peripheral_detach" then + ppm.handle_unmount(param1) + tool_ctl.gen_mon_list() + elseif event == "peripheral" then + ppm.mount(param1) + tool_ctl.gen_mon_list() + elseif event == "monitor_resize" then + tool_ctl.gen_mon_list() + elseif event == "modem_message" and tool_ctl.nic ~= nil and tool_ctl.net_listen then + local s_pkt = tool_ctl.nic.receive(param1, param2, param3, param4, param5) + + if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then + local mgmt_pkt = comms.mgmt_packet() + if mgmt_pkt.decode(s_pkt) then + tcd.abort(handle_timeout) + handle_packet(mgmt_pkt.get()) + end + end + 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/coordinator/coordinator.lua b/coordinator/coordinator.lua index 80c21b6..ae2ab77 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -9,11 +9,6 @@ local process = require("coordinator.process") local apisessions = require("coordinator.session.apisessions") -local dialog = require("coordinator.ui.dialog") - -local print = util.print -local println = util.println - local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK @@ -26,32 +21,75 @@ local LINK_TIMEOUT = 60.0 local coordinator = {} --- request the user to select a monitor ----@nodiscard ----@param names table available monitors ----@return boolean|string|nil -local function ask_monitor(names) - println("available monitors:") - for i = 1, #names do - print(" " .. names[i]) - end - println("") - println("select a monitor or type c to cancel") +---@type crd_config +local config = {} - local iface = dialog.ask_options(names, "c") +coordinator.config = config - if iface ~= false and iface ~= nil then - util.filter_table(names, function (x) return x ~= iface end) +-- load the coordinator configuration
+-- status of 0 is OK, 1 is bad config, 2 is bad monitor config +---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message) +function coordinator.load_config() + if not settings.load("/coordinator.settings") then return 1 end + + config.UnitCount = settings.get("UnitCount") + config.SpeakerVolume = settings.get("SpeakerVolume") + config.Time24Hour = settings.get("Time24Hour") + + config.DisableFlowView = settings.get("DisableFlowView") + config.MainDisplay = settings.get("MainDisplay") + config.FlowDisplay = settings.get("FlowDisplay") + config.UnitDisplays = settings.get("UnitDisplays") + + config.SVR_Channel = settings.get("SVR_Channel") + config.CRD_Channel = settings.get("CRD_Channel") + config.PKT_Channel = settings.get("PKT_Channel") + config.SVR_Timeout = settings.get("SVR_Timeout") + config.API_Timeout = settings.get("API_Timeout") + 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_int(config.UnitCount) + cfv.assert_range(config.UnitCount, 1, 4) + cfv.assert_type_bool(config.Time24Hour) + + cfv.assert_type_bool(config.DisableFlowView) + cfv.assert_type_table(config.UnitDisplays) + + cfv.assert_type_num(config.SpeakerVolume) + cfv.assert_range(config.SpeakerVolume, 0, 3) + + cfv.assert_channel(config.SVR_Channel) + cfv.assert_channel(config.CRD_Channel) + cfv.assert_channel(config.PKT_Channel) + + cfv.assert_type_num(config.SVR_Timeout) + cfv.assert_min(config.SVR_Timeout, 2) + cfv.assert_type_num(config.API_Timeout) + cfv.assert_min(config.API_Timeout, 2) + + cfv.assert_type_num(config.TrustedRange) + cfv.assert_min(config.TrustedRange, 0) + cfv.assert_type_str(config.AuthKey) + + if type(config.AuthKey) == "string" then + local len = string.len(config.AuthKey) + cfv.assert_eq(len == 0 or len >= 8, true) end - return iface -end + cfv.assert_type_int(config.LogMode) + cfv.assert_range(config.LogMode, 0, 1) + cfv.assert_type_str(config.LogPath) + cfv.assert_type_bool(config.LogDebug) + + -- Monitor Setup --- configure monitor layout ----@param num_units integer number of units expected ----@param disable_flow_view boolean disable flow view (legacy) ----@return boolean success, monitors_struct? monitors -function coordinator.configure_monitors(num_units, disable_flow_view) ---@class monitors_struct local monitors = { primary = nil, ---@type table|nil @@ -62,146 +100,73 @@ function coordinator.configure_monitors(num_units, disable_flow_view) unit_name_map = {} } - local monitors_avail = ppm.get_monitor_list() - local names = {} - local available = {} + local mon_cfv = util.new_validator() -- get all interface names - for iface, _ in pairs(monitors_avail) do - table.insert(names, iface) - table.insert(available, iface) - end + local names = {} + for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end - -- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display) - local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2) - if #names < num_displays_needed then - local message = "not enough monitors connected (need " .. num_displays_needed .. ")" - println(message) - log.warning(message) - return false - end + local function setup_monitors() + mon_cfv.assert_type_str(config.MainDisplay) + if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end + mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount) - -- attempt to load settings - if not settings.load("/coord.settings") then - log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)") - else - local _primary = settings.get("PRIMARY_DISPLAY") - local _flow = settings.get("FLOW_DISPLAY") - local _unitd = settings.get("UNIT_DISPLAYS") + if mon_cfv.valid() then + local w, h, _ - -- filter out already assigned monitors - util.filter_table(available, function (x) return x ~= _primary end) - util.filter_table(available, function (x) return x ~= _flow end) - if type(_unitd) == "table" then - util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end) - end - end - - --------------------- - -- PRIMARY DISPLAY -- - --------------------- - - local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil - - if not util.table_contains(names, iface_primary_display) then - println("primary display is not connected") - local response = dialog.ask_y_n("would you like to change it", true) - if response == false then return false end - iface_primary_display = nil - end - - while iface_primary_display == nil and #available > 0 do - iface_primary_display = ask_monitor(available) - end - - if type(iface_primary_display) ~= "string" then return false end - - settings.set("PRIMARY_DISPLAY", iface_primary_display) - util.filter_table(available, function (x) return x ~= iface_primary_display end) - - monitors.primary = ppm.get_periph(iface_primary_display) - monitors.primary_name = iface_primary_display - - -------------------------- - -- FLOW MONITOR DISPLAY -- - -------------------------- - - if not disable_flow_view then - local iface_flow_display = settings.get("FLOW_DISPLAY") ---@type boolean|string|nil - - if not util.table_contains(names, iface_flow_display) then - println("flow monitor display is not connected") - local response = dialog.ask_y_n("would you like to change it", true) - if response == false then return false end - iface_flow_display = nil - end - - while iface_flow_display == nil and #available > 0 do - iface_flow_display = ask_monitor(available) - end - - if type(iface_flow_display) ~= "string" then return false end - - settings.set("FLOW_DISPLAY", iface_flow_display) - util.filter_table(available, function (x) return x ~= iface_flow_display end) - - monitors.flow = ppm.get_periph(iface_flow_display) - monitors.flow_name = iface_flow_display - end - - ------------------- - -- UNIT DISPLAYS -- - ------------------- - - local unit_displays = settings.get("UNIT_DISPLAYS") - - if unit_displays == nil then - unit_displays = {} - for i = 1, num_units do - local display = nil - - while display == nil and #available > 0 do - println("please select monitor for unit #" .. i) - display = ask_monitor(available) + if not util.table_contains(names, config.MainDisplay) then + return 2, "Main monitor is not connected." end - if display == false then return false end + monitors.primary = ppm.get_periph(config.MainDisplay) + monitors.primary_name = config.MainDisplay - unit_displays[i] = display - end - else - -- make sure all displays are connected - for i = 1, num_units do - local display = unit_displays[i] - - if not util.table_contains(names, display) then - println("unit #" .. i .. " display is not connected") - local response = dialog.ask_y_n("would you like to change it", true) - if response == false then return false end - display = nil + monitors.primary.setTextScale(0.5) + w, _ = ppm.monitor_block_size(monitors.primary.getSize()) + if w ~= 8 then + return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).") end - while display == nil and #available > 0 do - display = ask_monitor(available) + if not config.DisableFlowView then + if not util.table_contains(names, config.FlowDisplay) then + return 2, "Flow monitor is not connected." + end + + monitors.flow = ppm.get_periph(config.FlowDisplay) + monitors.flow_name = config.FlowDisplay + + monitors.flow.setTextScale(0.5) + w, _ = ppm.monitor_block_size(monitors.flow.getSize()) + if w ~= 8 then + return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).") + end end - if display == false then return false end + for i = 1, config.UnitCount do + local display = config.UnitDisplays[i] + if type(display) ~= "string" or not util.table_contains(names, display) then + return 2, "Unit " .. i .. " monitor is not connected." + end - unit_displays[i] = display - end + monitors.unit_displays[i] = ppm.get_periph(display) + monitors.unit_name_map[i] = display + + monitors.unit_displays[i].setTextScale(0.5) + w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize()) + if w ~= 4 or h ~= 4 then + return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).") + end + end + else return 2, "Monitor configuration invalid." end end - settings.set("UNIT_DISPLAYS", unit_displays) - if not settings.save("/coord.settings") then - log.warning("configure_monitors(): failed to save coordinator settings file") - end + if cfv.valid() then + local ok, result, message = pcall(setup_monitors) + assert(ok, util.c("fatal error while trying to verify monitors: ", result)) + if result == 2 then return 2, message end + else return 1 end - for i = 1, #unit_displays do - monitors.unit_displays[i] = ppm.get_periph(unit_displays[i]) - monitors.unit_name_map[i] = unit_displays[i] - end - - return true, monitors + return 0, monitors end -- dmesg print wrapper @@ -246,13 +211,8 @@ end ---@nodiscard ---@param version string coordinator version ---@param nic nic network interface device ----@param num_units integer number of configured units for number of monitors, checked against SV ----@param crd_channel integer port of configured supervisor ----@param svr_channel integer listening port for supervisor replys ----@param pkt_channel integer listening port for pocket API ----@param range integer trusted device connection range ---@param sv_watchdog watchdog -function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) +function coordinator.comms(version, nic, sv_watchdog) local self = { sv_linked = false, sv_addr = comms.BROADCAST, @@ -267,16 +227,16 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk est_task_done = nil } - comms.set_trusted_range(range) - - -- PRIVATE FUNCTIONS -- + comms.set_trusted_range(config.TrustedRange) -- configure network channels nic.closeAll() - nic.open(crd_channel) + nic.open(config.CRD_Channel) - -- link nic to apisessions - apisessions.init(nic) + -- pass config to apisessions + apisessions.init(nic, config) + + -- PRIVATE FUNCTIONS -- -- send a packet to the supervisor ---@param msg_type MGMT_TYPE|CRDN_TYPE @@ -296,7 +256,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk pkt.make(msg_type, msg) s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) - nic.transmit(svr_channel, crd_channel, s_pkt) + nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt) self.sv_seq_num = self.sv_seq_num + 1 end @@ -310,7 +270,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk 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) + nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt) self.last_api_est_acks[packet.src_addr()] = ack end @@ -343,7 +303,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk self.est_last = self.est_start self.est_tick_waiting, self.est_task_done = - coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel) + coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel) _send_establish() else @@ -356,7 +316,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk if abort then coordinator.log_comms("supervisor connection attempt cancelled by user") elseif self.sv_config_err then - coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file") + coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs") elseif not self.sv_linked then if self.last_est_ack == ESTABLISH_ACK.DENY then coordinator.log_comms("supervisor connection attempt denied") @@ -371,7 +331,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk ok = false elseif self.sv_config_err then - coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file") + coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs") ok = false elseif (util.time_s() - self.est_last) > 1.0 then _send_establish() @@ -405,10 +365,10 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk end -- send the auto process control configuration with a start command - ---@param config coord_auto_config configuration - function public.send_auto_start(config) + ---@param auto_cfg coord_auto_config configuration + function public.send_auto_start(auto_cfg) _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 + FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits }) end @@ -464,9 +424,9 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk local src_addr = packet.scada_frame.src_addr() local protocol = packet.scada_frame.protocol() - if l_chan ~= crd_channel then + if l_chan ~= config.CRD_Channel then log.debug("received packet on unconfigured channel " .. l_chan, true) - elseif r_chan == pkt_channel then + elseif r_chan == config.PKT_Channel then if not self.sv_linked then log.debug("discarding pocket API packet before linked to supervisor") elseif protocol == PROTOCOL.SCADA_CRDN then @@ -526,7 +486,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk else log.debug("illegal packet type " .. protocol .. " on pocket channel", true) end - elseif r_chan == svr_channel then + elseif r_chan == config.SVR_Channel then -- check sequence number if self.sv_r_seq_num == nil then self.sv_r_seq_num = packet.scada_frame.seq_num() @@ -699,22 +659,22 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk -- connection with supervisor established if packet.length == 2 then local est_ack = packet.data[1] - local config = packet.data[2] + local sv_config = packet.data[2] if est_ack == ESTABLISH_ACK.ALLOW then -- reset to disconnected before validating iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) - if type(config) == "table" and #config == 2 then + if type(sv_config) == "table" and #sv_config == 2 then -- get configuration ---@class facility_conf local conf = { - num_units = config[1], ---@type integer - cooling = config[2] ---@type sv_cooling_conf + num_units = sv_config[1], ---@type integer + cooling = sv_config[2] ---@type sv_cooling_conf } - if conf.num_units == num_units then + if conf.num_units == config.UnitCount then -- init io controller iocontrol.init(conf, public) diff --git a/coordinator/process.lua b/coordinator/process.lua index 5551c93..581fcd9 100644 --- a/coordinator/process.lua +++ b/coordinator/process.lua @@ -19,15 +19,20 @@ local process = {} local self = { io = nil, ---@type ioctl comms = nil, ---@type coord_comms - ---@class coord_auto_config - config = { - mode = PROCESS.INACTIVE, - burn_target = 0.0, - charge_target = 0.0, - gen_target = 0.0, - limits = {}, - waste_product = PRODUCT.PLUTONIUM, - pu_fallback = false + ---@class coord_control_states + control_states = { + ---@class coord_auto_config + process = { + mode = PROCESS.INACTIVE, + burn_target = 0.0, + charge_target = 0.0, + gen_target = 0.0, + limits = {}, + waste_product = PRODUCT.PLUTONIUM, + pu_fallback = false + }, + waste_modes = {}, + priority_groups = {} } } @@ -42,63 +47,64 @@ function process.init(iocontrol, coord_comms) self.io = iocontrol self.comms = coord_comms + local ctl_proc = self.control_states.process + for i = 1, self.io.facility.num_units do - self.config.limits[i] = 0.1 + ctl_proc.limits[i] = 0.1 end - -- load settings - if not settings.load("/coord.settings") then - log.error("process.init(): failed to load coordinator settings file") - end + local ctrl_states = settings.get("ControlStates", {}) + local config = ctrl_states.process ---@type coord_auto_config -- facility auto control configuration - local config = settings.get("PROCESS") ---@type coord_auto_config|nil if type(config) == "table" then - self.config.mode = config.mode - self.config.burn_target = config.burn_target - self.config.charge_target = config.charge_target - self.config.gen_target = config.gen_target - self.config.limits = config.limits - self.config.waste_product = config.waste_product - self.config.pu_fallback = config.pu_fallback + ctl_proc.mode = config.mode + ctl_proc.burn_target = config.burn_target + ctl_proc.charge_target = config.charge_target + ctl_proc.gen_target = config.gen_target + ctl_proc.limits = config.limits + ctl_proc.waste_product = config.waste_product + ctl_proc.pu_fallback = config.pu_fallback - self.io.facility.ps.publish("process_mode", self.config.mode) - self.io.facility.ps.publish("process_burn_target", self.config.burn_target) - self.io.facility.ps.publish("process_charge_target", self.config.charge_target) - self.io.facility.ps.publish("process_gen_target", self.config.gen_target) - self.io.facility.ps.publish("process_waste_product", self.config.waste_product) - self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback) + self.io.facility.ps.publish("process_mode", ctl_proc.mode) + self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) + self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target) + self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) + self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product) + self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback) - for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do + for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do local unit = self.io.units[id] ---@type ioctl_unit - unit.unit_ps.publish("burn_limit", self.config.limits[id]) + unit.unit_ps.publish("burn_limit", ctl_proc.limits[id]) end - log.info("PROCESS: loaded auto control settings from coord.settings") + log.info("PROCESS: loaded auto control settings") -- notify supervisor of auto waste config - self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product) - self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback) + self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product) + self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback) end -- unit waste states - local waste_modes = settings.get("WASTE_MODES") ---@type table|nil + local waste_modes = ctrl_states.waste_modes ---@type table|nil if type(waste_modes) == "table" then for id, mode in pairs(waste_modes) do + self.control_states.waste_modes[id] = mode self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) end - log.info("PROCESS: loaded unit waste mode settings from coord.settings") + log.info("PROCESS: loaded unit waste mode settings") end -- unit priority groups - local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil + local prio_groups = ctrl_states.priority_groups ---@type table|nil if type(prio_groups) == "table" then for id, group in pairs(prio_groups) do + self.control_states.priority_groups[id] = group self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) end - log.info("PROCESS: loaded priority groups settings from coord.settings") + log.info("PROCESS: loaded priority groups settings") end end @@ -155,15 +161,10 @@ function process.set_unit_waste(id, mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode)) - local waste_mode = settings.get("WASTE_MODES") ---@type table|nil + self.control_states.waste_modes[id] = mode + settings.set("ControlStates", self.control_states) - if type(waste_mode) ~= "table" then waste_mode = {} end - - waste_mode[id] = mode - - settings.set("WASTE_MODES", waste_mode) - - if not settings.save("/coord.settings") then + if not settings.save("/coordinator.settings") then log.error("process.set_unit_waste(): failed to save coordinator settings file") end end @@ -198,15 +199,10 @@ function process.set_group(unit_id, group_id) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id) log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id)) - local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil + self.control_states.priority_groups[unit_id] = group_id + settings.set("ControlStates", self.control_states) - if type(prio_groups) ~= "table" then prio_groups = {} end - - prio_groups[unit_id] = group_id - - settings.set("PRIORITY_GROUPS", prio_groups) - - if not settings.save("/coord.settings") then + if not settings.save("/coordinator.settings") then log.error("process.set_group(): failed to save coordinator settings file") end end @@ -217,20 +213,14 @@ end -- write auto process control to config file local function _write_auto_config() - -- attempt to load settings - if not settings.load("/coord.settings") then - log.warning("process._write_auto_config(): failed to load coordinator settings file") - end - -- save config - settings.set("PROCESS", self.config) - local saved = settings.save("/coord.settings") - + settings.set("ControlStates", self.control_states) + local saved = settings.save("/coordinator.settings") if not saved then log.warning("process._write_auto_config(): failed to save coordinator settings file") end - return not not saved + return saved end -- stop automatic process control @@ -241,7 +231,7 @@ end -- start automatic process control function process.start_auto() - self.comms.send_auto_start(self.config) + self.comms.send_auto_start(self.control_states.process) log.debug("PROCESS: START AUTO CTL") end @@ -253,7 +243,7 @@ function process.set_process_waste(product) log.debug(util.c("PROCESS: SET WASTE ", product)) -- update config table and save - self.config.waste_product = product + self.control_states.process.waste_product = product _write_auto_config() end @@ -265,7 +255,7 @@ function process.set_pu_fallback(enabled) log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled)) -- update config table and save - self.config.pu_fallback = enabled + self.control_states.process.pu_fallback = enabled _write_auto_config() end @@ -279,11 +269,12 @@ function process.save(mode, burn_target, charge_target, gen_target, limits) log.debug("PROCESS: SAVE") -- update config table - self.config.mode = mode - self.config.burn_target = burn_target - self.config.charge_target = charge_target - self.config.gen_target = gen_target - self.config.limits = limits + local ctl_proc = self.control_states.process + ctl_proc.mode = mode + ctl_proc.burn_target = burn_target + ctl_proc.charge_target = charge_target + ctl_proc.gen_target = gen_target + ctl_proc.limits = limits -- save config self.io.facility.save_cfg_ack(_write_auto_config()) @@ -294,22 +285,23 @@ end function process.start_ack_handle(response) local ack = response[1] - self.config.mode = response[2] - self.config.burn_target = response[3] - self.config.charge_target = response[4] - self.config.gen_target = response[5] + local ctl_proc = self.control_states.process + ctl_proc.mode = response[2] + ctl_proc.burn_target = response[3] + ctl_proc.charge_target = response[4] + ctl_proc.gen_target = response[5] for i = 1, math.min(#response[6], self.io.facility.num_units) do - self.config.limits[i] = response[6][i] + ctl_proc.limits[i] = response[6][i] local unit = self.io.units[i] ---@type ioctl_unit - unit.unit_ps.publish("burn_limit", self.config.limits[i]) + unit.unit_ps.publish("burn_limit", ctl_proc.limits[i]) end - self.io.facility.ps.publish("process_mode", self.config.mode) - self.io.facility.ps.publish("process_burn_target", self.config.burn_target) - self.io.facility.ps.publish("process_charge_target", self.config.charge_target) - self.io.facility.ps.publish("process_gen_target", self.config.gen_target) + self.io.facility.ps.publish("process_mode", ctl_proc.mode) + self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) + self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target) + self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target) self.io.facility.start_ack(ack) end @@ -317,14 +309,14 @@ end -- record waste product state after attempting to change it ---@param response WASTE_PRODUCT supervisor waste product state function process.waste_ack_handle(response) - self.config.waste_product = response + self.control_states.process.waste_product = response self.io.facility.ps.publish("process_waste_product", response) end -- record plutonium fallback state after attempting to change it ---@param response boolean supervisor plutonium fallback state function process.pu_fb_ack_handle(response) - self.config.pu_fallback = response + self.control_states.process.pu_fallback = response self.io.facility.ps.publish("process_pu_fallback", response) end diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index 5aadbb2..07282c4 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -3,7 +3,6 @@ -- local log = require("scada-common.log") -local util = require("scada-common.util") local iocontrol = require("coordinator.iocontrol") @@ -93,39 +92,6 @@ function renderer.init_displays() end end --- check main display width ----@nodiscard ----@return boolean width_okay -function renderer.validate_main_display_width() - local w, _ = engine.monitors.primary.getSize() - return w == 164 -end - --- check flow display width ----@nodiscard ----@return boolean width_okay -function renderer.validate_flow_display_width() - local w, _ = engine.monitors.flow.getSize() - return w == 164 -end - --- check display sizes ----@nodiscard ----@return boolean valid all unit display dimensions OK -function renderer.validate_unit_display_sizes() - local valid = true - - for id, monitor in ipairs(engine.monitors.unit_displays) do - local w, h = monitor.getSize() - if w ~= 79 or h ~= 52 then - log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h)) - valid = false - end - end - - return valid -end - -- initialize the dmesg output window function renderer.init_dmesg() local disp_x, disp_y = engine.monitors.primary.getSize() diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index 6e8c771..516b91b 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -3,7 +3,6 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") -local config = require("coordinator.config") local iocontrol = require("coordinator.iocontrol") local pocket = require("coordinator.session.pocket") @@ -11,7 +10,8 @@ local pocket = require("coordinator.session.pocket") local apisessions = {} local self = { - nic = nil, + nic = nil, ---@type nic + config = nil, ---@type crd_config next_id = 0, sessions = {} } @@ -32,7 +32,7 @@ local function _api_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message) + self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -59,7 +59,7 @@ local function _shutdown(session) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message) + self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message) end end @@ -69,9 +69,11 @@ end -- PUBLIC FUNCTIONS -- -- initialize apisessions ----@param nic nic -function apisessions.init(nic) +---@param nic nic network interface +---@param config crd_config coordinator config +function apisessions.init(nic, config) self.nic = nic + self.config = config end -- find a session by remote port @@ -103,7 +105,7 @@ function apisessions.establish_session(source_addr, version) local id = self.next_id - pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT) + pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout) table.insert(self.sessions, pkt_s) local mt = { diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 3a52029..5831b8a 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -14,7 +14,7 @@ local util = require("scada-common.util") local core = require("graphics.core") -local config = require("coordinator.config") +local configure = require("coordinator.configure") local coordinator = require("coordinator.coordinator") local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") @@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.1.0" +local COORDINATOR_VERSION = "v1.2.2" local println = util.println local println_ts = util.println_ts @@ -34,32 +34,34 @@ local log_comms = coordinator.log_comms local log_crypto = coordinator.log_crypto ---------------------------------------- --- config validation +-- get configuration ---------------------------------------- -local cfv = util.new_validator() +-- mount connected devices (required for monitor setup) +ppm.mount_all() -cfv.assert_channel(config.SVR_CHANNEL) -cfv.assert_channel(config.CRD_CHANNEL) -cfv.assert_channel(config.PKT_CHANNEL) -cfv.assert_type_int(config.TRUSTED_RANGE) -cfv.assert_type_num(config.SV_TIMEOUT) -cfv.assert_min(config.SV_TIMEOUT, 2) -cfv.assert_type_num(config.API_TIMEOUT) -cfv.assert_min(config.API_TIMEOUT, 2) -cfv.assert_type_int(config.NUM_UNITS) -cfv.assert_type_num(config.SOUNDER_VOLUME) -cfv.assert_type_bool(config.TIME_24_HOUR) -cfv.assert_type_str(config.LOG_PATH) -cfv.assert_type_int(config.LOG_MODE) +local loaded, monitors = coordinator.load_config() +if loaded ~= 0 then + -- try to reconfigure (user action) + local success, error = configure.configure(loaded, monitors) + if success then + loaded, monitors = coordinator.load_config() + assert(loaded == 0, util.trinary(loaded == 1, "failed to load valid configuration", "monitor configuration invalid")) + else + assert(success, "coordinator configuration error: " .. error) + end +end -assert(cfv.valid(), "bad config file: missing/invalid fields") +-- passed checks, good now +---@cast monitors monitors_struct + +local config = coordinator.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 coordinator.startup " .. COORDINATOR_VERSION) @@ -77,39 +79,16 @@ local function main() -- system startup ---------------------------------------- - -- mount connected devices + -- re-mount devices now that logging is ready ppm.mount_all() -- report versions/init fp PSIL iocontrol.init_fp(COORDINATOR_VERSION, comms.version) - -- setup monitors - local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS, config.DISABLE_FLOW_VIEW == true) - if not configured or monitors == nil then - println("startup> monitor setup failed") - log.fatal("monitor configuration failed") - return - end - -- init renderer - renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true) + renderer.legacy_disable_flow_view(config.DisableFlowView) renderer.set_displays(monitors) renderer.init_displays() - - if not renderer.validate_main_display_width() then - println("startup> main display must be 8 blocks wide") - log.fatal("main display not wide enough") - return - elseif (config.DISABLE_FLOW_VIEW ~= true) and not renderer.validate_flow_display_width() then - println("startup> flow display must be 8 blocks wide") - log.fatal("flow display not wide enough") - return - elseif not renderer.validate_unit_display_sizes() then - println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks") - log.fatal("unit display dimensions incorrect") - return - end - renderer.init_dmesg() -- lets get started! @@ -132,7 +111,7 @@ local function main() else local sounder_start = util.time_ms() log_boot("annunciator alarm speaker connected") - sounder.init(speaker, config.SOUNDER_VOLUME) + sounder.init(speaker, config.SpeakerVolume) log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_sys("annunciator alarm configured") iocontrol.fp_has_speaker(true) @@ -143,8 +122,8 @@ local function main() ---------------------------------------- -- message authentication init - if type(config.AUTH_KEY) == "string" then - local init_time = network.init_mac(config.AUTH_KEY) + if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then + local init_time = network.init_mac(config.AuthKey) log_crypto("HMAC init took " .. init_time .. "ms") end @@ -161,14 +140,13 @@ local function main() end -- create connection watchdog - local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT) + local conn_watchdog = util.new_watchdog(config.SVR_Timeout) conn_watchdog.cancel() log.debug("startup> conn watchdog created") -- create network interface then setup comms local nic = network.nic(modem) - local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL, - config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog) + local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") @@ -214,7 +192,7 @@ local function main() local link_failed = false local ui_ok = true - local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") + local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") -- start clock loop_clock.start() diff --git a/coordinator/ui/dialog.lua b/coordinator/ui/dialog.lua deleted file mode 100644 index 676ae2b..0000000 --- a/coordinator/ui/dialog.lua +++ /dev/null @@ -1,52 +0,0 @@ -local completion = require("cc.completion") - -local util = require("scada-common.util") - -local print = util.print - -local dialog = {} - --- ask the user yes or no ----@nodiscard ----@param question string ----@param default boolean ----@return boolean|nil -function dialog.ask_y_n(question, default) - print(question) - - if default == true then - print(" (Y/n)? ") - else - print(" (y/N)? ") - end - - local response = read(nil, nil) - - if response == "" then - return default - elseif response == "Y" or response == "y" then - return true - elseif response == "N" or response == "n" then - return false - else - return nil - end -end - --- ask the user for an input within a set of options ----@nodiscard ----@param options table ----@param cancel string ----@return boolean|string|nil -function dialog.ask_options(options, cancel) - print("> ") - local response = read(nil, nil, function(text) return completion.choice(text, options) end) - - if response == cancel then return false end - - if util.table_contains(options, response) then - return response - else return nil end -end - -return dialog diff --git a/graphics/core.lua b/graphics/core.lua index 13f97ec..3e53b83 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -7,7 +7,7 @@ local flasher = require("graphics.flasher") local core = {} -core.version = "2.1.0" +core.version = "2.1.1" core.flasher = flasher core.events = events diff --git a/graphics/element.lua b/graphics/element.lua index 6debaf4..939efd6 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -843,9 +843,12 @@ function element.new(args, child_offset_x, child_offset_y) -- re-draw this element and all its children function public.redraw() + local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor() protected.window.setBackgroundColor(protected.fg_bg.bkg) protected.window.setTextColor(protected.fg_bg.fgd) protected.window.clear() + protected.window.setBackgroundColor(bg) + protected.window.setTextColor(fg) protected.redraw() for _, child in pairs(protected.children) do child.get().redraw() end end diff --git a/pocket/config.lua b/pocket/config.lua deleted file mode 100644 index 72625f4..0000000 --- a/pocket/config.lua +++ /dev/null @@ -1,27 +0,0 @@ -local config = {} - --- supervisor comms channel -config.SVR_CHANNEL = 16240 --- coordinator comms channel -config.CRD_CHANNEL = 16243 --- pocket comms channel -config.PKT_CHANNEL = 16244 --- 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/pocket/configure.lua b/pocket/configure.lua new file mode 100644 index 0000000..6c0f728 --- /dev/null +++ b/pocket/configure.lua @@ -0,0 +1,578 @@ +-- +-- 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 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 tri = util.trinary + +local cpair = core.cpair + +local LEFT = core.ALIGN.LEFT +local CENTER = core.ALIGN.CENTER +local RIGHT = core.ALIGN.RIGHT + +-- changes to the config data/format to let the user know +local changes = {} + +---@class pkt_configurator +local configurator = {} + +local style = {} + +style.root = cpair(colors.black, colors.lightGray) +style.header = cpair(colors.white, colors.gray) + +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 bw_fg_bg = cpair(colors.black, colors.white) +local g_lg_fg_bg = cpair(colors.gray, colors.lightGray) +local nav_fg_bg = bw_fg_bg +local btn_act_fg_bg = cpair(colors.white, colors.gray) +local dis_fg_bg = cpair(colors.lightGray,colors.white) + +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 pkt_config +local tmp_cfg = { + SVR_Channel = nil, ---@type integer + CRD_Channel = nil, ---@type integer + PKT_Channel = nil, ---@type integer + ConnTimeout = nil, ---@type number + TrustedRange = nil, ---@type number + AuthKey = nil, ---@type string|nil + LogMode = 0, + LogPath = "", + LogDebug = false, +} + +---@class pkt_config +local ini_cfg = {} +---@class pkt_config +local settings_cfg = {} + +-- all settings fields, their nice names, and their default values +local fields = { + { "SVR_Channel", "SVR Channel", 16240 }, + { "CRD_Channel", "CRD Channel", 16243 }, + { "PKT_Channel", "PKT Channel", 16244 }, + { "ConnTimeout", "Connection Timeout", 5 }, + { "TrustedRange", "Trusted Range", 0 }, + { "AuthKey", "Facility Auth Key" , ""}, + { "LogMode", "Log Mode", log.MODE.APPEND }, + { "LogPath", "Log Path", "/log.txt" }, + { "LogDebug","Log Debug Messages", false } +} + +-- load data from the settings file +---@param target pkt_config +---@param raw boolean? true to not use default values +local function load_settings(target, raw) + for _, v in pairs(fields) do settings.unset(v[1]) end + + local loaded = settings.load("/pocket.settings") + + for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end + + return loaded +end + +-- create the config view +---@param display graphics_element +local function config_view(display) +---@diagnostic disable-next-line: undefined-field + local function exit() os.queueEvent("terminate") end + + TextBox{parent=display,y=1,text="Pocket 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 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 changelog = Div{parent=root_pane_div,x=1,y=1} + + local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}} + + -- Main Page + + local y_start = 7 + + TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."} + + if tool_ctl.ask_config then + TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",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(settings_cfg) + tool_ctl.settings_apply.hide(true) + main_pane.set_value(4) + end + + if fs.exists("/pocket/config.lua") then + PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",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 Device",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=dis_fg_bg} + + if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end + + PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} + PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#region Network + + local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24} + local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24} + local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24} + local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24} + + local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}} + + TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)} + + TextBox{parent=net_c_1,x=1,y=1,height=1,text="Set network channels."} + TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,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=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"} + local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"} + local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg} + TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg} + + local chan_err = TextBox{parent=net_c_1,x=1,y=14,height=1,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_channels() + local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value()) + if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then + tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c + net_pane.set_value(2) + chan_err.hide(true) + else chan_err.show() end + end + + PushButton{parent=net_c_1,x=1,y=15,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=net_c_1,x=19,y=15,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="Set connection timeout."} + TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_2,x=1,y=11,height=1,width=19,text="Connection Timeout"} + local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg} + + TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg} + + local ct_err = TextBox{parent=net_c_2,x=1,y=14,height=1,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_timeouts() + local timeout_val = tonumber(timeout.get_value()) + if timeout_val ~= nil then + tmp_cfg.ConnTimeout = timeout_val + net_pane.set_value(3) + ct_err.hide(true) + else ct_err.show() end + end + + PushButton{parent=net_c_2,x=1,y=15,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=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_3,x=1,y=1,height=1,text="Set the trusted range."} + TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg} + TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg} + + local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg} + + local tr_err = TextBox{parent=net_c_3,x=1,y=14,height=1,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_tr() + local range_val = tonumber(range.get_value()) + if range_val ~= nil then + tmp_cfg.TrustedRange = range_val + net_pane.set_value(4) + tr_err.hide(true) + else tr_err.show() end + end + + PushButton{parent=net_c_3,x=1,y=15,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=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."} + TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg} + + TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"} + local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg} + + local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end + + -- declare back first so tabbing makes sense visually + PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key} + + hide_key.set_value(true) + censor_key(true) + + local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} + + local function submit_auth() + local v = key.get_value() + if string.len(v) == 0 or string.len(v) >= 8 then + tmp_cfg.AuthKey = key.get_value() + main_pane.set_value(3) + key_err.hide(true) + else key_err.show() end + end + + PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Logging + + local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24} + + TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)} + + TextBox{parent=log_c_1,x=1,y=1,height=1,text="Configure logging below."} + + TextBox{parent=log_c_1,x=1,y=3,height=1,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="Log File Path"} + local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg} + + local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)} + TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg} + + local path_err = TextBox{parent=log_c_1,x=1,y=14,height=1,width=24,text="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(4) + else path_err.show() end + end + + PushButton{parent=log_c_1,x=1,y=15,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=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + --#endregion + + --#region Summary and Saving + + local sum_c_1 = Div{parent=summary,x=2,y=4,width=24} + local sum_c_2 = Div{parent=summary,x=2,y=4,width=24} + local sum_c_3 = Div{parent=summary,x=2,y=4,width=24} + local sum_c_4 = Div{parent=summary,x=2,y=4,width=24} + + 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=" Summary",fg_bg=cpair(colors.black,colors.green)} + + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + local function back_from_summary() + 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(3) + 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("/pocket.settings") then + load_settings(settings_cfg, true) + load_settings(ini_cfg) + + try_set(svr_chan, ini_cfg.SVR_Channel) + try_set(crd_chan, ini_cfg.CRD_Channel) + try_set(pkt_chan, ini_cfg.PKT_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) + + tool_ctl.view_cfg.enable() + + 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=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,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=dis_fg_bg} + tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,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="Settings saved!"} + + local function go_home() + main_pane.set_value(1) + net_pane.set_value(1) + sum_pane.set_value(1) + end + + PushButton{parent=sum_c_2,x=1,y=15,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=19,y=15,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=4,text="The old config.lua file will now be deleted, then the configurator will exit."} + + local function delete_legacy() + fs.delete("/pocket/config.lua") + exit() + end + + PushButton{parent=sum_c_3,x=1,y=15,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=19,y=15,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=8,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=15,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=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)} + + --#endregion + + -- Config Change Log + + local cl = Div{parent=changelog,x=2,y=4,width=24} + + TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} + + local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + + for _, change in ipairs(changes) do + TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} + for _, v in ipairs(change[2]) do + local e = Div{parent=c_log,height=#util.strwrap(v,21)} + TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)} + TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)} + end + end + + PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + + -- set tool functions now that we have the elements + + -- load a legacy config file + function tool_ctl.load_legacy() + local config = require("pocket.config") + + tmp_cfg.SVR_Channel = config.SVR_CHANNEL + tmp_cfg.CRD_Channel = config.CRD_CHANNEL + tmp_cfg.PKT_Channel = config.PKT_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(4) + 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 pkt_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)) + elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end + + if val == "nil" then val = "" end + + local c = util.trinary(alternate, g_lg_fg_bg, 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 pcoket configurator +---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration +function configurator.configure(ask_config) + tool_ctl.ask_config = ask_config == true + + load_settings(settings_cfg, true) + tool_ctl.has_config = 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 + tcd.handle(param1) + elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then + 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 + 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 + 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/pocket/pocket.lua b/pocket/pocket.lua index b2da51d..0bec6ba 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -13,17 +13,57 @@ local LINK_STATE = iocontrol.LINK_STATE local pocket = {} +---@type pkt_config +local config = {} + +pocket.config = config + +-- load the pocket configuration +function pocket.load_config() + if not settings.load("/pocket.settings") then return false end + + config.SVR_Channel = settings.get("SVR_Channel") + config.CRD_Channel = settings.get("CRD_Channel") + config.PKT_Channel = settings.get("PKT_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_channel(config.SVR_Channel) + cfv.assert_channel(config.CRD_Channel) + cfv.assert_channel(config.PKT_Channel) + cfv.assert_type_num(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) + + if type(config.AuthKey) == "string" then + local len = string.len(config.AuthKey) + cfv.assert(len == 0 or len >= 8) + end + + cfv.assert_type_int(config.LogMode) + cfv.assert_range(config.LogMode, 0, 1) + cfv.assert_type_str(config.LogPath) + cfv.assert_type_bool(config.LogDebug) + + return cfv.valid() +end + -- pocket coordinator + supervisor communications ---@nodiscard ---@param version string pocket version ---@param nic nic network interface device ----@param pkt_channel integer pocket comms channel ----@param svr_channel integer supervisor access channel ----@param crd_channel integer coordinator access channel ----@param range integer trusted device connection range ---@param sv_watchdog watchdog ---@param api_watchdog watchdog -function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) +function pocket.comms(version, nic, sv_watchdog, api_watchdog) local self = { sv = { linked = false, @@ -42,13 +82,13 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range establish_delay_counter = 0 } - comms.set_trusted_range(range) + comms.set_trusted_range(config.TrustedRange) -- PRIVATE FUNCTIONS -- -- configure network channels nic.closeAll() - nic.open(pkt_channel) + nic.open(config.PKT_Channel) -- send a management packet to the supervisor ---@param msg_type MGMT_TYPE @@ -60,7 +100,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range pkt.make(msg_type, msg) s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - nic.transmit(svr_channel, pkt_channel, s_pkt) + nic.transmit(config.SVR_Channel, config.PKT_Channel, s_pkt) self.sv.seq_num = self.sv.seq_num + 1 end @@ -74,7 +114,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range pkt.make(msg_type, msg) s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - nic.transmit(crd_channel, pkt_channel, s_pkt) + nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt) self.api.seq_num = self.api.seq_num + 1 end @@ -217,9 +257,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range local protocol = packet.scada_frame.protocol() local src_addr = packet.scada_frame.src_addr() - if l_chan ~= pkt_channel then + if l_chan ~= config.PKT_Channel then log.debug("received packet on unconfigured channel " .. l_chan, true) - elseif r_chan == crd_channel then + elseif r_chan == config.CRD_Channel then -- check sequence number if self.api.r_seq_num == nil then self.api.r_seq_num = packet.scada_frame.seq_num() @@ -308,7 +348,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range else log.debug("illegal packet type " .. protocol .. " from coordinator", true) end - elseif r_chan == svr_channel then + elseif r_chan == config.SVR_Channel then -- check sequence number if self.sv.r_seq_num == nil then self.sv.r_seq_num = packet.scada_frame.seq_num() diff --git a/pocket/startup.lua b/pocket/startup.lua index 4929955..13ac7fd 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -13,38 +13,37 @@ local util = require("scada-common.util") local core = require("graphics.core") -local config = require("pocket.config") +local configure = require("pocket.configure") local iocontrol = require("pocket.iocontrol") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "v0.6.3-alpha" +local POCKET_VERSION = "v0.7.0-alpha" local println = util.println local println_ts = util.println_ts ---------------------------------------- --- config validation +-- get configuration ---------------------------------------- -local cfv = util.new_validator() +if not pocket.load_config() then + -- try to reconfigure (user action) + local success, error = configure.configure(true) + if success then + assert(pocket.load_config(), "failed to load valid configuration") + else + assert(success, "pocket configuration error: " .. error) + end +end -cfv.assert_channel(config.SVR_CHANNEL) -cfv.assert_channel(config.CRD_CHANNEL) -cfv.assert_channel(config.PKT_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") +local config = pocket.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 pocket.startup " .. POCKET_VERSION) @@ -69,8 +68,8 @@ local function main() ---------------------------------------- -- message authentication init - if type(config.AUTH_KEY) == "string" then - network.init_mac(config.AUTH_KEY) + if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then + network.init_mac(config.AuthKey) end iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) @@ -85,8 +84,8 @@ local function main() -- create connection watchdogs local conn_wd = { - sv = util.new_watchdog(config.COMMS_TIMEOUT), - api = util.new_watchdog(config.COMMS_TIMEOUT) + sv = util.new_watchdog(config.ConnTimeout), + api = util.new_watchdog(config.ConnTimeout) } conn_wd.sv.cancel() @@ -96,8 +95,7 @@ local function main() -- create network interface then setup comms local nic = network.nic(modem) - local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL, - config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) + local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api) log.debug("startup> comms init") -- base loop clock (2Hz, 10 ticks) diff --git a/reactor-plc/configure.lua b/reactor-plc/configure.lua index fbefb97..db9162d 100644 --- a/reactor-plc/configure.lua +++ b/reactor-plc/configure.lua @@ -181,7 +181,7 @@ local function config_view(display) local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary,changelog}} - -- MAIN PAGE + -- Main Page local y_start = 5 @@ -212,7 +212,7 @@ local function config_view(display) 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} PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- PLC CONFIG + --#region PLC 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} @@ -290,7 +290,9 @@ local function config_view(display) PushButton{parent=plc_c_4,x=1,y=14,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,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- NET CONFIG + --#endregion + + --#region Network 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} @@ -390,7 +392,9 @@ local function config_view(display) PushButton{parent=net_c_3,x=1,y=14,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,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- LOG CONFIG + --#endregion + + --#region Logging local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} @@ -430,7 +434,9 @@ local function config_view(display) PushButton{parent=log_c_1,x=1,y=14,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,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- SUMMARY OF CHANGES + --#endregion + + --#region Summary and Saving 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} @@ -441,7 +447,7 @@ local function config_view(display) TextBox{parent=summary,x=1,y=2,height=1,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=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function back_from_settings() if tool_ctl.viewing_config or tool_ctl.importing_legacy then @@ -463,7 +469,7 @@ local function config_view(display) local function save_and_continue() for k, v in pairs(tmp_cfg) do settings.set(k, v) end - if settings.save("reactor-plc.settings") then + if settings.save("/reactor-plc.settings") then load_settings(settings_cfg, true) load_settings(ini_cfg) @@ -525,13 +531,15 @@ local function config_view(display) 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)} - -- CONFIG CHANGE LOG + --#endregion + + -- Config Change Log local cl = Div{parent=changelog,x=2,y=4,width=49} TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} - local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} for _, change in ipairs(changes) do TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} @@ -646,7 +654,7 @@ local function reset_term() 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 +---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration function configurator.configure(ask_config) tool_ctl.ask_config = ask_config == true diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 7287d3d..c5e6be3 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -37,14 +37,17 @@ function plc.load_config() config.Networked = settings.get("Networked") config.UnitID = settings.get("UnitID") + config.EmerCoolEnable = settings.get("EmerCoolEnable") config.EmerCoolSide = settings.get("EmerCoolSide") config.EmerCoolColor = settings.get("EmerCoolColor") + config.SVR_Channel = settings.get("SVR_Channel") config.PLC_Channel = settings.get("PLC_Channel") config.ConnTimeout = settings.get("ConnTimeout") config.TrustedRange = settings.get("TrustedRange") config.AuthKey = settings.get("AuthKey") + config.LogMode = settings.get("LogMode") config.LogPath = settings.get("LogPath") config.LogDebug = settings.get("LogDebug") @@ -71,6 +74,7 @@ function plc.load_config() end cfv.assert_type_int(config.LogMode) + cfv.assert_range(config.LogMode, 0, 1) cfv.assert_type_str(config.LogPath) cfv.assert_type_bool(config.LogDebug) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1e066d0..e667e46 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.6.9" +local R_PLC_VERSION = "v1.6.11" local println = util.println local println_ts = util.println_ts @@ -31,7 +31,7 @@ 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") + assert(plc.load_config(), "failed to load valid configuration") else assert(success, "reactor PLC configuration error: " .. error) end diff --git a/rtu/configure.lua b/rtu/configure.lua index c1ab9db..131623b 100644 --- a/rtu/configure.lua +++ b/rtu/configure.lua @@ -273,7 +273,7 @@ local function config_view(display) local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,summary,changelog,peri_cfg,rs_cfg}} - --#region MAIN PAGE + --#region Main Page local y_start = 2 @@ -324,7 +324,7 @@ local function config_view(display) --#endregion - --#region SPEAKER CONFIG + --#region Speakers local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} @@ -353,7 +353,7 @@ local function config_view(display) --#endregion - --#region NET CONFIG + --#region Network 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} @@ -455,7 +455,7 @@ local function config_view(display) --#endregion - --#region LOG CONFIG + --#region Logging local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} @@ -494,7 +494,7 @@ local function config_view(display) --#endregion - --#region SUMMARY OF CHANGES + --#region Summary and Saving 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} @@ -508,7 +508,7 @@ local function config_view(display) TextBox{parent=summary,x=1,y=2,height=1,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=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function back_from_settings() if tool_ctl.viewing_config or tool_ctl.importing_legacy then @@ -539,7 +539,7 @@ local function config_view(display) if settings.get("Peripherals") == nil then settings.set("Peripherals", {}) end if settings.get("Redstone") == nil then settings.set("Redstone", {}) end - if settings.save("rtu.settings") then + if settings.save("/rtu.settings") then load_settings(settings_cfg, true) load_settings(ini_cfg) @@ -578,13 +578,13 @@ local function config_view(display) tool_ctl.settings_confirm.hide() TextBox{parent=sum_c_2,x=1,y=1,height=1,text="The following peripherals will be imported:"} - local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=51,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local peri_import_list = ListBox{parent=sum_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} PushButton{parent=sum_c_2,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_2,x=41,y=14,min_width=9,text="Confirm",callback=function()sum_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg} TextBox{parent=sum_c_3,x=1,y=1,height=1,text="The following redstone entries will be imported:"} - local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=51,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local rs_import_list = ListBox{parent=sum_c_3,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} PushButton{parent=sum_c_3,x=1,y=14,text="\x1b Back",callback=function()sum_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sum_c_3,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} @@ -614,13 +614,13 @@ local function config_view(display) --#endregion - --#region CONFIG CHANGE LOG + --#region Config Change Log local cl = Div{parent=changelog,x=2,y=4,width=49} TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} - local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} for _, change in ipairs(changes) do TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} @@ -635,7 +635,7 @@ local function config_view(display) --#endregion - --#region DEVICES + --#region Peripherals local peri_c_1 = Div{parent=peri_cfg,x=2,y=4,width=49} local peri_c_2 = Div{parent=peri_cfg,x=2,y=4,width=49} @@ -649,7 +649,7 @@ local function config_view(display) TextBox{parent=peri_cfg,x=1,y=2,height=1,text=" Peripheral Connections",fg_bg=cpair(colors.black,colors.purple)} - local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=51,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local peri_list = ListBox{parent=peri_c_1,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function peri_revert() tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals) @@ -659,7 +659,7 @@ local function config_view(display) local function peri_apply() settings.set("Peripherals", tmp_cfg.Peripherals) - if settings.save("rtu.settings") then + if settings.save("/rtu.settings") then load_settings(settings_cfg, true) load_settings(ini_cfg) peri_pane.set_value(5) @@ -675,7 +675,7 @@ local function config_view(display) TextBox{parent=peri_c_2,x=1,y=1,height=1,text="Select one of the below devices to use."} - tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=51,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + tool_ctl.ppm_devs = ListBox{parent=peri_c_2,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} PushButton{parent=peri_c_2,x=1,y=14,text="\x1b Back",callback=function()peri_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=peri_c_2,x=8,y=14,min_width=10,text="Manual +",callback=function()peri_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.orange),active_fg_bg=btn_act_fg_bg} @@ -949,7 +949,7 @@ local function config_view(display) --#endregion - --#region REDSTONE + --#region Redstone local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49} @@ -963,7 +963,7 @@ local function config_view(display) TextBox{parent=rs_cfg,x=1,y=2,height=1,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} TextBox{parent=rs_c_1,x=1,y=1,height=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} - local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=51,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function rs_revert() tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone) @@ -973,7 +973,7 @@ local function config_view(display) local function rs_apply() settings.set("Redstone", tmp_cfg.Redstone) - if settings.save("rtu.settings") then + if settings.save("/rtu.settings") then load_settings(settings_cfg, true) load_settings(ini_cfg) rs_pane.set_value(4) @@ -992,7 +992,7 @@ local function config_view(display) TextBox{parent=rs_c_2,x=1,y=1,height=1,text="Select one of the below ports to use."} - local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=51,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function new_rs(port) if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then @@ -1466,7 +1466,7 @@ local function reset_term() end -- run the RTU gateway configurator ----@param ask_config? boolean indicate if this is being called by the RTU startup app due to an invalid configuration +---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration function configurator.configure(ask_config) tool_ctl.ask_config = ask_config == true diff --git a/rtu/rtu.lua b/rtu/rtu.lua index b76ce30..6c2b01f 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -41,6 +41,8 @@ function rtu.load_config() local cfv = util.new_validator() cfv.assert_type_num(config.SpeakerVolume) + cfv.assert_range(config.SpeakerVolume, 0, 3) + cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(config.RTU_Channel) cfv.assert_type_num(config.ConnTimeout) @@ -55,6 +57,7 @@ function rtu.load_config() end cfv.assert_type_int(config.LogMode) + cfv.assert_range(config.LogMode, 0, 1) cfv.assert_type_str(config.LogPath) cfv.assert_type_bool(config.LogDebug) diff --git a/rtu/startup.lua b/rtu/startup.lua index 0a3753d..d58639c 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.7.11" +local RTU_VERSION = "v1.7.13" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -47,7 +47,7 @@ if not rtu.load_config() then -- try to reconfigure (user action) local success, error = configure.configure(true) if success then - assert(rtu.load_config(), "failed to load valid RTU configuration") + assert(rtu.load_config(), "failed to load valid configuration") else assert(success, "RTU configuration error: " .. error) end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index edcb7c3..eaa7ec4 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,7 +17,7 @@ local max_distance = nil local comms = {} -- protocol/data version (protocol/data independent changes tracked by util.lua version) -comms.version = "2.4.3" +comms.version = "2.4.4" ---@enum PROTOCOL local PROTOCOL = { diff --git a/scada-common/network.lua b/scada-common/network.lua index d9fa83f..742f418 100644 --- a/scada-common/network.lua +++ b/scada-common/network.lua @@ -7,7 +7,7 @@ local log = require("scada-common.log") local util = require("scada-common.util") local md5 = require("lockbox.digest.md5") -local sha256 = require("lockbox.digest.sha2_256") +local sha1 = require("lockbox.digest.sha1") local pbkdf2 = require("lockbox.kdf.pbkdf2") local hmac = require("lockbox.mac.hmac") local stream = require("lockbox.util.stream") @@ -31,12 +31,12 @@ function network.init_mac(passkey) local key_deriv = pbkdf2() -- setup PBKDF2 - key_deriv.setPassword(passkey) + key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha1)) + key_deriv.setBlockLen(20) + key_deriv.setDKeyLen(20) + key_deriv.setIterations(256) key_deriv.setSalt("pepper") - key_deriv.setIterations(32) - key_deriv.setBlockLen(8) - key_deriv.setDKeyLen(16) - key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256)) + key_deriv.setPassword(passkey) key_deriv.finish() c_eng.key = array.fromHex(key_deriv.asHex()) diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index d54fec8..df64b68 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -421,4 +421,15 @@ function ppm.get_monitor_list() return list end +-- HELPER FUNCTIONS + +-- get the block size of a monitor given its width and height at a text scale of 0.5 +---@nodiscard +---@param width integer character width +---@param height integer character height +---@return integer block_width, integer block_height +function ppm.monitor_block_size(width, height) + return math.floor((width - 15) / 21) + 1, math.floor((height - 10) / 14) + 1 +end + return ppm diff --git a/scada-common/util.lua b/scada-common/util.lua index 28974de..10b4202 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.1.12" +util.version = "1.1.14" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -348,6 +348,16 @@ function util.table_contains(t, element) return false end +-- count the length of a table, even if the values are not sequential or contain named keys +---@nodiscard +---@param t table +---@return integer length +function util.table_len(t) + local n = 0 + for _, _ in pairs(t) do n = n + 1 end + return n +end + --#endregion --#region MEKANISM POWER diff --git a/startup.lua b/startup.lua index 7dd95f6..811f510 100644 --- a/startup.lua +++ b/startup.lua @@ -2,7 +2,7 @@ local util = require("scada-common.util") local println = util.println -local BOOTLOADER_VERSION = "0.5" +local BOOTLOADER_VERSION = "1.0" println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION) println("BOOT> SCANNING FOR APPLICATIONS...") diff --git a/supervisor/configure.lua b/supervisor/configure.lua index 745c082..34398b6 100644 --- a/supervisor/configure.lua +++ b/supervisor/configure.lua @@ -170,7 +170,7 @@ local function config_view(display) local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,svr_cfg,net_cfg,log_cfg,summary,changelog,import_err}} - -- MAIN PAGE + -- Main Page local y_start = 5 @@ -201,7 +201,7 @@ local function config_view(display) 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} PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- SUPERVISOR CONFIG + --#region Facility local svr_c_1 = Div{parent=svr_cfg,x=2,y=4,width=49} local svr_c_2 = Div{parent=svr_cfg,x=2,y=4,width=49} @@ -331,7 +331,7 @@ local function config_view(display) TextBox{parent=div,x=1,y=1,width=33,height=1,text="Unit "..i.." will be connected to..."} TextBox{parent=div,x=6,y=2,width=3,height=1,text="..."} - local tank_opt = Radio2D{parent=div,x=10,y=2,rows=1,columns=2,default=val,options={"its own Unit Tank","a Facility Tank"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} + local tank_opt = Radio2D{parent=div,x=9,y=2,rows=1,columns=2,default=val,options={"its own Unit Tank","a Facility Tank"},radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.yellow,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} local no_tank = TextBox{parent=div,x=9,y=2,width=34,height=1,text="no tank (as you set two steps ago)",fg_bg=cpair(colors.gray,colors.lightGray),hidden=true} tool_ctl.tank_elems[i] = { div = div, tank_opt = tank_opt, no_tank = no_tank } @@ -564,7 +564,9 @@ local function config_view(display) PushButton{parent=svr_c_6,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- NET CONFIG + --#endregion + + --#region Network 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} @@ -692,7 +694,9 @@ local function config_view(display) PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- LOG CONFIG + --#endregion + + --#region Logging local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} @@ -728,7 +732,9 @@ local function config_view(display) PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- SUMMARY OF CHANGES + --#endregion + + --#region Summary and Saving 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} @@ -739,7 +745,7 @@ local function config_view(display) TextBox{parent=summary,x=1,y=2,height=1,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=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} local function back_from_settings() if tool_ctl.viewing_config or tool_ctl.importing_legacy then @@ -761,7 +767,7 @@ local function config_view(display) local function save_and_continue() for k, v in pairs(tmp_cfg) do settings.set(k, v) end - if settings.save("supervisor.settings") then + if settings.save("/supervisor.settings") then load_settings(settings_cfg, true) load_settings(ini_cfg) @@ -838,13 +844,15 @@ local function config_view(display) 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)} - -- CONFIG CHANGE LOG + --#endregion + + -- Config Change Log local cl = Div{parent=changelog,x=2,y=4,width=49} TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg} - local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} + local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} for _, change in ipairs(changes) do TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} @@ -857,7 +865,7 @@ local function config_view(display) PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} - -- IMPORT ERROR + -- Import Error local i_err = Div{parent=import_err,x=2,y=4,width=49} @@ -968,7 +976,7 @@ local function config_view(display) if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") - elseif f[1] == "CoolingConfig" and cfg.CoolingConfig then + elseif f[1] == "CoolingConfig" and type(cfg.CoolingConfig) == "table" then val = "" for idx = 1, #cfg.CoolingConfig do @@ -982,7 +990,7 @@ local function config_view(display) if val == "" then val = "no facility tanks" end elseif f[1] == "FacilityTankMode" and raw == 0 then val = "0 (n/a, unit mode)" - elseif f[1] == "FacilityTankDefs" and cfg.FacilityTankDefs then + elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then val = "" for idx = 1, #cfg.FacilityTankDefs do @@ -1033,7 +1041,7 @@ local function reset_term() end -- run the supervisor configurator ----@param ask_config? boolean indicate if this is being called by the supervisor startup app due to an invalid configuration +---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration function configurator.configure(ask_config) tool_ctl.ask_config = ask_config == true diff --git a/supervisor/facility.lua b/supervisor/facility.lua index ce98d1f..94f93b3 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -337,7 +337,7 @@ function facility.new(num_reactors, cooling_conf) if state_changed then self.saturated = false - log.debug("FAC: state changed from " .. PROCESS_NAMES[self.last_mode + 1] .. " to " .. PROCESS_NAMES[self.mode + 1]) + log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1])) if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then self.start_fail = START_STATUS.OK @@ -375,6 +375,8 @@ function facility.new(num_reactors, cooling_conf) end end + log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t")) + if blade_count == nil then -- no units log.warning("FAC: cannot start process control with 0 units assigned") @@ -436,7 +438,7 @@ function facility.new(num_reactors, cooling_conf) self.saturated = true self.status_text = { "MONITORED MODE", "running reactors at limit" } - log.info(util.c("FAC: MAX_BURN process mode started")) + log.info("FAC: MAX_BURN process mode started") end _allocate_burn_rate(self.max_burn_combined, true) @@ -445,7 +447,7 @@ function facility.new(num_reactors, cooling_conf) if state_changed then self.time_start = now self.status_text = { "BURN RATE MODE", "running" } - log.info(util.c("FAC: BURN_RATE process mode started")) + log.info("FAC: BURN_RATE process mode started") end local unallocated = _allocate_burn_rate(self.burn_target, true) @@ -459,7 +461,7 @@ function facility.new(num_reactors, cooling_conf) self.accumulator = 0 self.status_text = { "CHARGE MODE", "running control loop" } - log.info(util.c("FAC: CHARGE mode starting PID control")) + log.info("FAC: CHARGE mode starting PID control") elseif self.last_update ~= charge_update then -- convert to kFE to make constants not microscopic local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000 @@ -614,7 +616,7 @@ function facility.new(num_reactors, cooling_conf) astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE) if was_fill and not astatus.matrix_fill then - log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%") + log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%")) end -- check for critical unit alarms diff --git a/supervisor/startup.lua b/supervisor/startup.lua index e4ab974..69633fa 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.2.5" +local SUPERVISOR_VERSION = "v1.2.8" local println = util.println local println_ts = util.println_ts @@ -34,7 +34,7 @@ if not supervisor.load_config() then -- try to reconfigure (user action) local success, error = configure.configure(true) if success then - assert(supervisor.load_config(), "failed to load valid supervisor configuration") + assert(supervisor.load_config(), "failed to load valid configuration") else assert(success, "supervisor configuration error: " .. error) end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 755893f..27bf22c 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -69,6 +69,7 @@ function supervisor.load_config() cfv.assert_min(config.PKT_Timeout, 2) cfv.assert_type_num(config.TrustedRange) + cfv.assert_min(config.TrustedRange, 0) if type(config.AuthKey) == "string" then local len = string.len(config.AuthKey) @@ -76,6 +77,7 @@ function supervisor.load_config() end cfv.assert_type_int(config.LogMode) + cfv.assert_range(config.LogMode, 0, 1) cfv.assert_type_str(config.LogPath) cfv.assert_type_bool(config.LogDebug) @@ -197,9 +199,8 @@ function supervisor.comms(_version, nic, fp_ok) -- pass the packet onto the session handler session.in_queue.push_packet(packet) else - -- unknown session, force a re-link - log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") - _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + -- any other packet should be session related, discard it + log.debug("discarding RPLC packet without a known session") end elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 645f4e9..8d38222 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -564,6 +564,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) function public.auto_engage() self.auto_engaged = true if self.plc_i ~= nil then + log.debug(util.c("UNIT ", self.r_id, ": engaged auto control")) self.plc_i.auto_lock(true) end end @@ -572,6 +573,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) function public.auto_disengage() self.auto_engaged = false if self.plc_i ~= nil then + log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control")) self.plc_i.auto_lock(false) self.db.control.br100 = 0 end @@ -582,12 +584,12 @@ function unit.new(reactor_id, num_boilers, num_turbines) ---@nodiscard ---@return integer lim_br100 function public.auto_get_effective_limit() - if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then - self.db.control.br100 = 0 + local ctrl = self.db.control + if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then + -- log.debug(util.c("UNIT ", self.r_id, ": effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]")) + ctrl.br100 = 0 return 0 - else - return self.db.control.lim_br100 - end + else return ctrl.lim_br100 end end -- set the automatic burn rate based on the last set burn rate in 100ths @@ -595,8 +597,8 @@ function unit.new(reactor_id, num_boilers, num_turbines) function public.auto_commit_br100(ramp) if self.auto_engaged then if self.plc_i ~= nil then + log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp)) self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp) - if ramp then self.ramp_target_br100 = self.db.control.br100 end end end