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..7c3fe58 --- /dev/null +++ b/coordinator/configure.lua @@ -0,0 +1,1322 @@ +-- +-- 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 = { + {"v1.2.4", { "Added temperature scale options" } } +} + +---@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, + TempScale = 1, + 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 }, + { "TempScale", "Temperature Scale", 1 }, + { "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) else network.deinit_mac() 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} + + TextBox{parent=crd_c_1,x=1,y=8,height=1,text="Temperature Scale"} + local temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options={"Kelvin","Celsius","Fahrenheit","Rankine"},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 + tmp_cfg.TempScale = temp_scale.get_value() + 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] == "TempScale" then + if raw == 1 then val = "Kelvin" elseif raw == 2 then val = "Celsius" elseif raw == 3 then val = "Fahrenheit" else val = "Rankine" end + 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..bc97c99 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,182 +21,155 @@ 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.TempScale = settings.get("TempScale") + + 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_int(config.TempScale) + cfv.assert_range(config.TempScale, 1, 4) + + 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 - primary_name = "", - flow = nil, ---@type table|nil + main = nil, ---@type table|nil + main_name = "", + flow = nil, ---@type table|nil flow_name = "", unit_displays = {}, 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.main = ppm.get_periph(config.MainDisplay) + monitors.main_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.main.setTextScale(0.5) + w, _ = ppm.monitor_block_size(monitors.main.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 +214,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 +230,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 +259,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 +273,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 @@ -339,24 +302,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk if not self.sv_linked then if self.est_tick_waiting == nil then - self.est_start = util.time_s() + self.est_start = os.clock() 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 - self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start))) + self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (os.clock() - self.est_start))) end - if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then + if abort or (os.clock() - self.est_start) >= LINK_TIMEOUT then self.est_task_done(false) 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,11 +334,11 @@ 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 + elseif (os.clock() - self.est_last) > 1.0 then _send_establish() - self.est_last = util.time_s() + self.est_last = os.clock() end elseif self.est_tick_waiting ~= nil then self.est_task_done(true) @@ -405,10 +368,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 +427,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 +489,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,24 +662,24 @@ 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) + iocontrol.init(conf, public, config.TempScale) self.sv_addr = src_addr self.sv_linked = true diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 3e32d6d..312d194 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -47,7 +47,23 @@ end -- initialize the coordinator IO controller ---@param conf facility_conf configuration ---@param comms coord_comms comms reference -function iocontrol.init(conf, comms) +---@param temp_scale integer temperature unit (1 = K, 2 = C, 3 = F, 4 = R) +function iocontrol.init(conf, comms, temp_scale) + -- temperature unit label and conversion function (from Kelvin) + if temp_scale == 2 then + io.temp_label = "\xb0C" + io.temp_convert = function (t) return t - 273.15 end + elseif temp_scale == 3 then + io.temp_label = "\xb0F" + io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end + elseif temp_scale == 4 then + io.temp_label = "\xb0R" + io.temp_convert = function (t) return 1.8 * t end + else + io.temp_label = "K" + io.temp_convert = function (t) return t end + end + -- facility data structure ---@class ioctl_facility io.facility = { @@ -219,7 +235,10 @@ function iocontrol.init(conf, comms) control_state = false, burn_rate_cmd = 0.0, radiation = types.new_zero_radiation_reading(), - sna_prod_rate = 0.0, + + sna_peak_rate = 0.0, + sna_max_rate = 0.0, + sna_out_rate = 0.0, waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM, @@ -923,7 +942,7 @@ function iocontrol.update_unit_statuses(statuses) local boil_sum = 0 for id = 1, #unit.boiler_ps_tbl do - if rtu_statuses.boilers[i] == nil then + if rtu_statuses.boilers[id] == nil then -- disconnected unit.boiler_ps_tbl[id].publish("computed_status", 1) end @@ -966,7 +985,7 @@ function iocontrol.update_unit_statuses(statuses) local flow_sum = 0 for id = 1, #unit.turbine_ps_tbl do - if rtu_statuses.turbines[i] == nil then + if rtu_statuses.turbines[id] == nil then -- disconnected unit.turbine_ps_tbl[id].publish("computed_status", 1) end @@ -1009,7 +1028,7 @@ function iocontrol.update_unit_statuses(statuses) -- dynamic tank statuses if type(rtu_statuses.tanks) == "table" then for id = 1, #unit.tank_ps_tbl do - if rtu_statuses.tanks[i] == nil then + if rtu_statuses.tanks[id] == nil then -- disconnected unit.tank_ps_tbl[id].publish("computed_status", 1) end @@ -1048,12 +1067,14 @@ function iocontrol.update_unit_statuses(statuses) -- solar neutron activator status info if type(rtu_statuses.sna) == "table" then unit.num_snas = rtu_statuses.sna[1] ---@type integer - unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number - unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number + unit.sna_peak_rate = rtu_statuses.sna[2] ---@type number + unit.sna_max_rate = rtu_statuses.sna[3] ---@type number + unit.sna_out_rate = rtu_statuses.sna[4] ---@type number unit.unit_ps.publish("sna_count", unit.num_snas) - unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate) unit.unit_ps.publish("sna_peak_rate", unit.sna_peak_rate) + unit.unit_ps.publish("sna_max_rate", unit.sna_max_rate) + unit.unit_ps.publish("sna_out_rate", unit.sna_out_rate) sna_count_sum = sna_count_sum + unit.num_snas else @@ -1201,7 +1222,7 @@ function iocontrol.update_unit_statuses(statuses) local u_spent_rate = waste_rate local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0) - local u_po_rate = util.trinary(not is_pu, math.min(waste_rate, unit.sna_prod_rate), 0.0) + local u_po_rate = unit.sna_out_rate unit.unit_ps.publish("pu_rate", u_pu_rate) unit.unit_ps.publish("po_rate", u_po_rate) @@ -1209,14 +1230,15 @@ function iocontrol.update_unit_statuses(statuses) unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate)) if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then + u_spent_rate = u_po_rate unit.unit_ps.publish("po_pl_rate", u_po_rate) unit.unit_ps.publish("po_am_rate", 0) po_pl_rate = po_pl_rate + u_po_rate elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then + u_spent_rate = 0 unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_am_rate", u_po_rate) po_am_rate = po_am_rate + u_po_rate - u_spent_rate = 0 else unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_am_rate", 0) 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..541b1a7 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") @@ -53,6 +52,16 @@ local function _init_display(monitor) end end +-- print out that the monitor is too small +---@param monitor table monitor +local function _print_too_small(monitor) + monitor.setCursorPos(1, 1) + monitor.setBackgroundColor(colors.black) + monitor.setTextColor(colors.red) + monitor.clear() + monitor.write("monitor too small") +end + -- disable the flow view ---@param disable boolean function renderer.legacy_disable_flow_view(disable) @@ -65,15 +74,15 @@ function renderer.set_displays(monitors) engine.monitors = monitors -- report to front panel as connected - iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil) + iocontrol.fp_monitor_state("main", engine.monitors.main ~= nil) iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil) for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end end -- init all displays in use by the renderer function renderer.init_displays() - -- init primary and flow monitors - _init_display(engine.monitors.primary) + -- init main and flow monitors + _init_display(engine.monitors.main) if not engine.disable_flow_view then _init_display(engine.monitors.flow) end -- init unit displays @@ -93,43 +102,10 @@ 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() - engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y) + local disp_w, disp_h = engine.monitors.main.getSize() + engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h) log.direct_dmesg(engine.dmesg_window) end @@ -200,8 +176,8 @@ function renderer.try_start_ui() status, msg = pcall(function () -- show main view on main monitor - if engine.monitors.primary ~= nil then - engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} + if engine.monitors.main ~= nil then + engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root} main_view(engine.ui.main_display) end @@ -276,43 +252,43 @@ function renderer.ui_ready() return engine.ui_ready end function renderer.handle_disconnect(device) local is_used = false - if engine.monitors ~= nil then - if engine.monitors.primary == device then - if engine.ui.main_display ~= nil then - -- delete element tree and clear root UI elements - engine.ui.main_display.delete() - end + if not engine.monitors then return false end - is_used = true - engine.monitors.primary = nil - engine.ui.main_display = nil + if engine.monitors.main == device then + if engine.ui.main_display ~= nil then + -- delete element tree and clear root UI elements + engine.ui.main_display.delete() + end - iocontrol.fp_monitor_state("main", false) - elseif engine.monitors.flow == device then - if engine.ui.flow_display ~= nil then - -- delete element tree and clear root UI elements - engine.ui.flow_display.delete() - end + is_used = true + engine.monitors.main = nil + engine.ui.main_display = nil - is_used = true - engine.monitors.flow = nil - engine.ui.flow_display = nil + iocontrol.fp_monitor_state("main", false) + elseif engine.monitors.flow == device then + if engine.ui.flow_display ~= nil then + -- delete element tree and clear root UI elements + engine.ui.flow_display.delete() + end - iocontrol.fp_monitor_state("flow", false) - else - for idx, monitor in pairs(engine.monitors.unit_displays) do - if monitor == device then - if engine.ui.unit_displays[idx] ~= nil then - engine.ui.unit_displays[idx].delete() - end + is_used = true + engine.monitors.flow = nil + engine.ui.flow_display = nil - is_used = true - engine.monitors.unit_displays[idx] = nil - engine.ui.unit_displays[idx] = nil - - iocontrol.fp_monitor_state(idx, false) - break + iocontrol.fp_monitor_state("flow", false) + else + for idx, monitor in pairs(engine.monitors.unit_displays) do + if monitor == device then + if engine.ui.unit_displays[idx] ~= nil then + engine.ui.unit_displays[idx].delete() end + + is_used = true + engine.monitors.unit_displays[idx] = nil + engine.ui.unit_displays[idx] = nil + + iocontrol.fp_monitor_state(idx, false) + break end end end @@ -327,52 +303,29 @@ end function renderer.handle_reconnect(name, device) local is_used = false - if engine.monitors ~= nil then - if engine.monitors.primary_name == name then - is_used = true - _init_display(device) - engine.monitors.primary = device + if not engine.monitors then return false end - local disp_x, disp_y = engine.monitors.primary.getSize() - engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary) + -- note: handle_resize is a more adaptive way of re-initializing a connected monitor + -- since it can handle a monitor being reconnected that isn't the right size - if engine.ui_ready and (engine.ui.main_display == nil) then - engine.dmesg_window.setVisible(false) + if engine.monitors.main_name == name then + is_used = true + engine.monitors.main = device - engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root} - main_view(engine.ui.main_display) - else - engine.dmesg_window.setVisible(true) - engine.dmesg_window.redraw() - end + renderer.handle_resize(name) + elseif engine.monitors.flow_name == name then + is_used = true + engine.monitors.flow = device - iocontrol.fp_monitor_state("main", true) - elseif engine.monitors.flow_name == name then - is_used = true - _init_display(device) - engine.monitors.flow = device + renderer.handle_resize(name) + else + for idx, monitor in ipairs(engine.monitors.unit_name_map) do + if monitor == name then + is_used = true + engine.monitors.unit_displays[idx] = device - if engine.ui_ready and (engine.ui.flow_display == nil) then - engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root} - flow_view(engine.ui.flow_display) - end - - iocontrol.fp_monitor_state("flow", true) - else - for idx, monitor in ipairs(engine.monitors.unit_name_map) do - if monitor == name then - is_used = true - _init_display(device) - engine.monitors.unit_displays[idx] = device - - if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then - engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root} - unit_view(engine.ui.unit_displays[idx], idx) - end - - iocontrol.fp_monitor_state(idx, true) - break - end + renderer.handle_resize(name) + break end end end @@ -380,6 +333,137 @@ function renderer.handle_reconnect(name, device) return is_used end +-- handle a monitor being resized
+-- returns if this monitor is assigned + if the assigned screen still fits +---@param name string monitor name +---@return boolean is_used, boolean is_ok +function renderer.handle_resize(name) + local is_used = false + local is_ok = true + local ui = engine.ui + + if not engine.monitors then return false, false end + + if engine.monitors.main_name == name and engine.monitors.main then + local device = engine.monitors.main ---@type table + + -- this is necessary if the bottom left block was broken and on reconnect + _init_display(device) + + is_used = true + + -- resize dmesg window if needed, but don't make it thinner + local disp_w, disp_h = engine.monitors.main.getSize() + local dmsg_w, _ = engine.dmesg_window.getSize() + engine.dmesg_window.reposition(1, 1, math.max(disp_w, dmsg_w), disp_h, engine.monitors.main) + + if ui.main_display then + ui.main_display.delete() + ui.main_display = nil + end + + iocontrol.fp_monitor_state("main", true) + + engine.dmesg_window.setVisible(not engine.ui_ready) + + if engine.ui_ready then + local ok = pcall(function () + ui.main_display = DisplayBox{window=device,fg_bg=style.root} + main_view(ui.main_display) + end) + + if not ok then + if ui.main_display then + ui.main_display.delete() + ui.main_display = nil + end + + _print_too_small(device) + + iocontrol.fp_monitor_state("main", false) + is_ok = false + end + else engine.dmesg_window.redraw() end + elseif engine.monitors.flow_name == name and engine.monitors.flow then + local device = engine.monitors.flow ---@type table + + -- this is necessary if the bottom left block was broken and on reconnect + _init_display(device) + + is_used = true + + if ui.flow_display then + ui.flow_display.delete() + ui.flow_display = nil + end + + iocontrol.fp_monitor_state("flow", true) + + if engine.ui_ready then + engine.dmesg_window.setVisible(false) + + local ok = pcall(function () + ui.flow_display = DisplayBox{window=device,fg_bg=style.root} + flow_view(ui.flow_display) + end) + + if not ok then + if ui.flow_display then + ui.flow_display.delete() + ui.flow_display = nil + end + + _print_too_small(device) + + iocontrol.fp_monitor_state("flow", false) + is_ok = false + end + end + else + for idx, monitor in ipairs(engine.monitors.unit_name_map) do + local device = engine.monitors.unit_displays[idx] + + if monitor == name and device then + -- this is necessary if the bottom left block was broken and on reconnect + _init_display(device) + + is_used = true + + if ui.unit_displays[idx] then + ui.unit_displays[idx].delete() + ui.unit_displays[idx] = nil + end + + iocontrol.fp_monitor_state(idx, true) + + if engine.ui_ready then + engine.dmesg_window.setVisible(false) + + local ok = pcall(function () + ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root} + unit_view(ui.unit_displays[idx], idx) + end) + + if not ok then + if ui.unit_displays[idx] then + ui.unit_displays[idx].delete() + ui.unit_displays[idx] = nil + end + + _print_too_small(device) + + iocontrol.fp_monitor_state(idx, false) + is_ok = false + end + end + + break + end + end + end + + return is_used, is_ok +end -- handle a touch event ---@param event mouse_interaction|nil @@ -388,16 +472,15 @@ function renderer.handle_mouse(event) if engine.fp_ready and event.monitor == "terminal" then engine.ui.front_panel.handle_mouse(event) elseif engine.ui_ready then - if event.monitor == engine.monitors.primary_name then - engine.ui.main_display.handle_mouse(event) + if event.monitor == engine.monitors.main_name then + if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end elseif event.monitor == engine.monitors.flow_name then - engine.ui.flow_display.handle_mouse(event) + if engine.ui.flow_display then engine.ui.flow_display.handle_mouse(event) end else for id, monitor in ipairs(engine.monitors.unit_name_map) do - if event.monitor == monitor then - local layout = engine.ui.unit_displays[id] ---@type graphics_element - layout.handle_mouse(event) - break + local display = engine.ui.unit_displays[id] + if event.monitor == monitor and display then + if display then display.handle_mouse(event) end end end end 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..10294f8 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,9 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v1.1.0" +local COORDINATOR_VERSION = "v1.2.11" + +local CHUNK_LOAD_DELAY_S = 30.0 local println = util.println local println_ts = util.println_ts @@ -34,32 +36,66 @@ 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 wait_on_load = true +local loaded, monitors = coordinator.load_config() -assert(cfv.valid(), "bad config file: missing/invalid fields") +-- if the computer just started, its chunk may have just loaded (...or the user rebooted) +-- if monitor config failed, maybe an adjacent chunk containing all or part of a monitor has not loaded yet, so keep trying +while wait_on_load and loaded == 2 and os.clock() < CHUNK_LOAD_DELAY_S do + term.clear() + term.setCursorPos(1, 1) + println("There was a monitor configuration problem at boot.\n") + println("Startup will keep trying every 2s in case of chunk load delays.\n") + println(util.sprintf("The configurator will be started in %ds if all attempts fail.\n", math.max(0, CHUNK_LOAD_DELAY_S - os.clock()))) + println("(click to skip to the configurator)") + + local timer_id = util.start_timer(2) + + while true do + local event, param1 = util.pull_event() + if event == "timer" and param1 == timer_id then + -- remount and re-attempt + ppm.mount_all() + loaded, monitors = coordinator.load_config() + break + elseif event == "mouse_click" or event == "terminate" then + wait_on_load = false + break + end + end +end + +if loaded ~= 0 then + -- try to reconfigure (user action) + local success, error = configure.configure(loaded, monitors) + if success then + loaded, monitors = coordinator.load_config() + if loaded ~= 0 then + println(util.trinary(loaded == 2, "monitor configuration invalid", "failed to load a valid configuration") .. ", please reconfigure") + return + end + else + println("configuration error: " .. error) + return + end +end + +-- 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 +113,16 @@ local function main() -- system startup ---------------------------------------- - -- mount connected devices - ppm.mount_all() + -- log mounts now since mounting was done before logging was ready + ppm.log_mounts() -- 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 +145,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 +156,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 +174,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 +226,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() @@ -291,6 +303,11 @@ local function main() iocontrol.fp_has_speaker(true) end end + elseif event == "monitor_resize" then + local is_used, is_ok = renderer.handle_resize(param1) + if is_used then + log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits"))) + end elseif event == "timer" then if loop_clock.is_clock(param1) then -- main loop tick diff --git a/coordinator/ui/components/boiler.lua b/coordinator/ui/components/boiler.lua index 0d651d9..ce2f1bc 100644 --- a/coordinator/ui/components/boiler.lua +++ b/coordinator/ui/components/boiler.lua @@ -1,5 +1,7 @@ local style = require("coordinator.ui.style") +local iocontrol = require("coordinator.iocontrol") + local core = require("graphics.core") local Rectangle = require("graphics.elements.rectangle") @@ -13,6 +15,7 @@ local cpair = core.cpair local border = core.border local text_fg_bg = style.text_colors +local lu_col = style.lu_colors -- new boiler view ---@param root graphics_element parent @@ -20,14 +23,16 @@ local text_fg_bg = style.text_colors ---@param y integer top left y ---@param ps psil ps interface local function new_view(root, x, y, ps) + local db = iocontrol.get_db() + local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y} local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12} - local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=style.lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg} - local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} + local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=22,fg_bg=text_fg_bg} + local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg} status.register(ps, "computed_status", status.update) - temp.register(ps, "temperature", temp.update) + temp.register(ps, "temperature", function (t) temp.update(db.temp_convert(t)) end) boil_r.register(ps, "boil_rate", boil_r.update) TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg} diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua index c74ab0a..27b033e 100644 --- a/coordinator/ui/components/reactor.lua +++ b/coordinator/ui/components/reactor.lua @@ -1,5 +1,7 @@ local types = require("scada-common.types") +local iocontrol = require("coordinator.iocontrol") + local style = require("coordinator.ui.style") local core = require("graphics.core") @@ -23,15 +25,17 @@ local lu_col = style.lu_colors ---@param y integer top left y ---@param ps psil ps interface local function new_view(root, x, y, ps) + local db = iocontrol.get_db() + local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y} local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16} - local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} + local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg_bg} local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg} local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg} status.register(ps, "computed_status", status.update) - core_temp.register(ps, "temp", core_temp.update) + core_temp.register(ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) burn_r.register(ps, "act_burn_rate", burn_r.update) heating_r.register(ps, "heating_rate", heating_r.update) diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua index 52b633a..64fc1e0 100644 --- a/coordinator/ui/components/unit_detail.lua +++ b/coordinator/ui/components/unit_detail.lua @@ -3,6 +3,7 @@ -- local types = require("scada-common.types") +local util = require("scada-common.util") local iocontrol = require("coordinator.iocontrol") @@ -51,8 +52,9 @@ local period = core.flasher.PERIOD ---@param parent graphics_element parent ---@param id integer local function init(parent, id) - local unit = iocontrol.get_db().units[id] ---@type ioctl_unit - local f_ps = iocontrol.get_db().facility.ps + local db = iocontrol.get_db() + local unit = db.units[id] ---@type ioctl_unit + local f_ps = db.facility.ps local main = Div{parent=parent,x=1,y=1} @@ -114,8 +116,9 @@ local function init(parent, id) end) TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label} - local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} - core_temp.register(u_ps, "temp", core_temp.update) + local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f") + local core_temp = DataIndicator{parent=main,x=32,label="",format=fmt,value=0,commas=true,unit=db.temp_label,lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} + core_temp.register(u_ps, "temp", function (t) core_temp.update(db.temp_convert(t)) end) TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label} local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg} diff --git a/coordinator/ui/components/unit_flow.lua b/coordinator/ui/components/unit_flow.lua index f3103e5..0cb64c2 100644 --- a/coordinator/ui/components/unit_flow.lua +++ b/coordinator/ui/components/unit_flow.lua @@ -181,10 +181,10 @@ local function make(parent, x, y, wide, unit) local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} - local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg} + local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} + local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} + local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg} + local spent_rate = DataIndicator{parent=waste,x=_wide(117,98),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%8.3f",value=0,width=13,fg_bg=bw_fg_bg} waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update) pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update) @@ -214,7 +214,7 @@ local function make(parent, x, y, wide, unit) sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end) sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update) sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update) - sna_max.register(unit.unit_ps, "sna_prod_rate", sna_max.update) + sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update) sna_in.register(unit.unit_ps, "sna_in", sna_in.update) return root 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/coordinator/ui/layout/flow_view.lua b/coordinator/ui/layout/flow_view.lua index f19dda2..1040a8c 100644 --- a/coordinator/ui/layout/flow_view.lua +++ b/coordinator/ui/layout/flow_view.lua @@ -348,7 +348,7 @@ local function init(main) status.register(facility.sps_ps_tbl[1], "computed_status", status.update) TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label} - local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg} + local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.2f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg} sps_in.register(facility.ps, "po_am_rate", sps_in.update) @@ -370,8 +370,8 @@ local function init(main) TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray} local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg} local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17} - local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17} - local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17} + local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.2f",value=0,width=17} + local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.2f",value=0,width=17} pu.register(facility.ps, "pu_rate", pu.update) po.register(facility.ps, "po_rate", po.update) diff --git a/graphics/element.lua b/graphics/element.lua index 9280d4c..032cb81 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -845,9 +845,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 e2afe84..6f5b73c 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() @@ -310,7 +350,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 073db2f..763cc1a 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -13,38 +13,41 @@ 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.4-alpha" +local POCKET_VERSION = "v0.7.2-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 + if not pocket.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end + else + println("configuration error: " .. error) + return + 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 +72,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 +88,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 +99,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/databus.lua b/reactor-plc/databus.lua index ba7a68e..7436dbb 100644 --- a/reactor-plc/databus.lua +++ b/reactor-plc/databus.lua @@ -70,9 +70,9 @@ function databus.tx_link_state(state) end -- transmit reactor enable state across the bus ----@param active boolean reactor active +---@param active any reactor active function databus.tx_reactor_state(active) - databus.ps.publish("reactor_active", active) + databus.ps.publish("reactor_active", active == true) end -- transmit RPS data across the bus diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 7287d3d..eec1e4d 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) @@ -125,6 +129,21 @@ function plc.rps_init(reactor, is_formed) end end + -- check if the result of a peripheral call was OK, handle the failure if not + ---@nodiscard + ---@param result any PPM function call result + ---@return boolean succeeded if the result is OK, false if it was a PPM failure + local function _check_and_handle_ppm_call(result) + if result == ppm.ACCESS_FAULT then + _set_fault() + elseif result == ppm.UNDEFINED_FIELD then + _set_fault() + self.formed = false + else return true end + + return false + end + -- set emergency coolant control (if configured) ---@param state boolean true to enable emergency coolant, false to disable local function _set_emer_cool(state) @@ -163,25 +182,20 @@ function plc.rps_init(reactor, is_formed) -- check if the reactor is formed local function _is_formed() local formed = reactor.isFormed() - if formed == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - else + if _check_and_handle_ppm_call(formed) then self.formed = formed + end - if not self.state[state_keys.sys_fail] then - self.state[state_keys.sys_fail] = not formed - end + -- always update, since some ppm failures constitute not being formed + if not self.state[state_keys.sys_fail] then + self.state[state_keys.sys_fail] = not self.formed end end -- check if the reactor is force disabled local function _is_force_disabled() local disabled = reactor.isForceDisabled() - if disabled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - else + if _check_and_handle_ppm_call(disabled) then self.force_disabled = disabled if not self.state[state_keys.force_disabled] then @@ -193,22 +207,16 @@ function plc.rps_init(reactor, is_formed) -- check for high damage local function _high_damage() local damage_percent = reactor.getDamagePercent() - if damage_percent == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.high_dmg] then + if _check_and_handle_ppm_call(damage_percent) and not self.state[state_keys.high_dmg] then self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT end end -- check if the reactor is at a critically high temperature local function _high_temp() - -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 + -- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K local temp = reactor.getTemperature() - if temp == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.high_temp] then + if _check_and_handle_ppm_call(temp) and not self.state[state_keys.high_temp] then self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE end end @@ -216,10 +224,7 @@ function plc.rps_init(reactor, is_formed) -- check if there is very low coolant local function _low_coolant() local coolant_filled = reactor.getCoolantFilledPercentage() - if coolant_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.low_coolant] then + if _check_and_handle_ppm_call(coolant_filled) and not self.state[state_keys.low_coolant] then self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL end end @@ -227,10 +232,7 @@ function plc.rps_init(reactor, is_formed) -- check for excess waste (>80% filled) local function _excess_waste() local w_filled = reactor.getWasteFilledPercentage() - if w_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.ex_waste] then + if _check_and_handle_ppm_call(w_filled) and not self.state[state_keys.ex_waste] then self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL end end @@ -238,10 +240,7 @@ function plc.rps_init(reactor, is_formed) -- check for heated coolant backup (>95% filled) local function _excess_heated_coolant() local hc_filled = reactor.getHeatedCoolantFilledPercentage() - if hc_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.ex_hcoolant] then + if _check_and_handle_ppm_call(hc_filled) and not self.state[state_keys.ex_hcoolant] then self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL end end @@ -249,10 +248,7 @@ function plc.rps_init(reactor, is_formed) -- check if there is no fuel local function _insufficient_fuel() local fuel = reactor.getFuelFilledPercentage() - if fuel == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - _set_fault() - elseif not self.state[state_keys.no_fuel] then + if _check_and_handle_ppm_call(fuel) and not self.state[state_keys.no_fuel] then self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL end end @@ -473,13 +469,22 @@ function plc.rps_init(reactor, is_formed) self.tripped = false self.trip_cause = RPS_TRIP_CAUSE.OK - for i = 1, #self.state do - self.state[i] = false - end + for i = 1, #self.state do self.state[i] = false end if not quiet then log.info("RPS: reset") end end + -- partial RPS reset that only clears fault and sys_fail + function public.reset_formed() + self.tripped = false + self.trip_cause = RPS_TRIP_CAUSE.OK + + self.state[state_keys.fault] = false + self.state[state_keys.sys_fail] = false + + log.info("RPS: partial reset on formed") + end + -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause function public.auto_reset() self.state[state_keys.automatic] = false diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1e066d0..9c3d382 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.14" local println = util.println local println_ts = util.println_ts @@ -31,9 +31,13 @@ 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") + if not plc.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "reactor PLC configuration error: " .. error) + println("configuration error: " .. error) + return end end @@ -131,15 +135,22 @@ local function main() -- we need a reactor, can at least do some things even if it isn't formed though if plc_state.no_reactor then - println("init> fission reactor not found"); + println("init> fission reactor not found") log.warning("init> no reactor on startup") plc_state.init_ok = false plc_state.degraded = true elseif not smem_dev.reactor.isFormed() then - println("init> fission reactor not formed"); + println("init> fission reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed") + plc_state.degraded = true + plc_state.reactor_formed = false + elseif smem_dev.reactor.getStatus() == ppm.UNDEFINED_FIELD then + -- reactor formed after ppm.mount_all was called + println("init> fission reactor was not formed") + log.warning("init> reactor reported formed, but multiblock functions are not available") + plc_state.degraded = true plc_state.reactor_formed = false end diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 85e2a86..afe6acb 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -125,9 +125,8 @@ function threads.thread__main(smem, init) plc_comms.reconnect_reactor(plc_dev.reactor) end - -- reset RPS for newly connected reactor - -- without this, is_formed will be out of date and cause it to think its no longer formed again - rps.reset() + -- partial reset of RPS, specific to becoming formed + rps.reset_formed() else -- fully lost the reactor now :( println_ts("reactor lost (failed reconnect)!") @@ -231,9 +230,8 @@ function threads.thread__main(smem, init) plc_comms.reconnect_reactor(plc_dev.reactor) end - -- reset RPS for newly connected reactor - -- without this, is_formed will be out of date and cause it to think its no longer formed again - rps.reset() + -- partial reset of RPS, specific to becoming formed + rps.reset_formed() end elseif networked and type == "modem" then -- note, check init_ok first since nic will be nil if it is false 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..1c5a4a2 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.14" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -47,9 +47,13 @@ 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") + if not rtu.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "RTU configuration error: " .. error) + println("configuration error: " .. error) + return end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index edcb7c3..180e9c8 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.5" ---@enum PROTOCOL local PROTOCOL = { diff --git a/scada-common/network.lua b/scada-common/network.lua index d9fa83f..bdaf5c3 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()) @@ -53,6 +53,11 @@ function network.init_mac(passkey) return init_time end +-- de-initialize message authentication system +function network.deinit_mac() + c_eng.key, c_eng.hmac = nil, nil +end + -- generate HMAC of message ---@nodiscard ---@param message string initial value concatenated with ciphertext diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index d54fec8..fb98161 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -9,7 +9,7 @@ local util = require("scada-common.util") local ppm = {} local ACCESS_FAULT = nil ---@type nil -local UNDEFINED_FIELD = "undefined field" +local UNDEFINED_FIELD = "__PPM_UNDEF_FIELD__" local VIRTUAL_DEVICE_TYPE = "ppm_vdev" ppm.ACCESS_FAULT = ACCESS_FAULT @@ -155,7 +155,7 @@ local function peri_init(iface) self.fault_counts[key] = self.fault_counts[key] + 1 - return (function () return ACCESS_FAULT end) + return (function () return UNDEFINED_FIELD end) end } @@ -300,6 +300,17 @@ function ppm.handle_unmount(iface) return pm_type, pm_dev end +-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready +function ppm.log_mounts() + for iface, mount in pairs(ppm_sys.mounts) do + log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")")) + end + + if util.table_len(ppm_sys.mounts) == 0 then + log.warning("PPM: no devices had been found") + end +end + -- GENERAL ACCESSORS -- -- list all available peripherals @@ -421,4 +432,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..c72d62e 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.18" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -284,11 +284,13 @@ function util.cancel_timer(timer) os.cancelTimer(timer) end --#region PARALLELIZATION --- protected sleep call so we still are in charge of catching termination ----@param t integer seconds +-- protected sleep call so we still are in charge of catching termination
+-- returns the result of pcall +---@param t number seconds +---@return boolean success, any result, any ... --- EVENT_CONSUMER: this function consumes events ---@diagnostic disable-next-line: undefined-field -function util.psleep(t) pcall(os.sleep, t) end +function util.psleep(t) return pcall(os.sleep, t) end -- no-op to provide a brief pause (1 tick) to yield
--- EVENT_CONSUMER: this function consumes events @@ -348,6 +350,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..43c6cc4 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -152,6 +152,8 @@ function facility.new(num_reactors, cooling_conf) table.insert(self.test_tone_states, false) end + -- PRIVATE FUNCTIONS -- + -- check if all auto-controlled units completed ramping ---@nodiscard local function _all_units_ramped() @@ -228,7 +230,7 @@ function facility.new(num_reactors, cooling_conf) ---@class facility local public = {} - -- ADD/LINK DEVICES -- + --#region Add/Link Devices -- link a redstone RTU session ---@param rs_unit unit_session @@ -268,11 +270,9 @@ function facility.new(num_reactors, cooling_conf) for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end end - -- UPDATE -- + --#endregion - -- supervisor sessions reporting the list of active RTU sessions - ---@param rtu_sessions table session list of all connected RTUs - function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end + --#region Update -- update (iterate) the facility management function public.update() @@ -323,7 +323,7 @@ function facility.new(num_reactors, cooling_conf) -- Run Process Control -- ------------------------- - --#region Process Control + --#region local avg_charge = self.avg_charge.compute() local avg_inflow = self.avg_inflow.compute() @@ -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 @@ -595,7 +597,7 @@ function facility.new(num_reactors, cooling_conf) -- Evaluate Automatic SCRAM -- ------------------------------ - --#region Automatic SCRAM + --#region local astatus = self.ascram_status @@ -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 @@ -725,6 +727,8 @@ function facility.new(num_reactors, cooling_conf) -- Handle Redstone I/O -- ------------------------- + --#region + if #self.redstone > 0 then -- handle facility SCRAM if self.io_ctl.digital_read(IO.F_SCRAM) then @@ -754,10 +758,14 @@ function facility.new(num_reactors, cooling_conf) self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm) end + --#endregion + ---------------- -- Unit Tasks -- ---------------- + --#region + local insufficent_po_rate = false local need_emcool = false @@ -796,10 +804,14 @@ function facility.new(num_reactors, cooling_conf) end end + --#endregion + ------------------------ -- Update Alarm Tones -- ------------------------ + --#region + local allow_test = self.allow_testing and self.test_tone_set local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } @@ -886,6 +898,8 @@ function facility.new(num_reactors, cooling_conf) self.test_tone_set = false self.test_tone_reset = true end + + --#endregion end -- call the update function of all units in the facility
@@ -898,7 +912,9 @@ function facility.new(num_reactors, cooling_conf) end end - -- COMMANDS -- + --#endregion + + --#region Commands -- SCRAM all reactor units function public.scram_all() @@ -986,7 +1002,9 @@ function facility.new(num_reactors, cooling_conf) } end - -- SETTINGS -- + --#endregion + + --#region Settings -- set the automatic control group of a unit ---@param unit_id integer unit ID @@ -1027,7 +1045,9 @@ function facility.new(num_reactors, cooling_conf) return self.pu_fallback end - -- DIAGNOSTIC TESTING -- + --#endregion + + --#region Diagnostic Testing -- attempt to set a test tone state ---@param id TONE|0 tone ID or 0 to disable all @@ -1067,7 +1087,9 @@ function facility.new(num_reactors, cooling_conf) return self.allow_testing, self.test_alarm_states end - -- READ STATES/PROPERTIES -- + --#endregion + + --#region Read States/Properties -- get current alarm tone on/off states ---@nodiscard @@ -1181,6 +1203,12 @@ function facility.new(num_reactors, cooling_conf) return status end + --#endregion + + -- supervisor sessions reporting the list of active RTU sessions + ---@param rtu_sessions table session list of all connected RTUs + function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end + -- get the units in this facility ---@nodiscard function public.get_units() return self.units end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index e4ab974..72c0c97 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.11" local println = util.println local println_ts = util.println_ts @@ -34,9 +34,13 @@ 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") + if not supervisor.load_config() then + println("failed to load a valid configuration, please reconfigure") + return + end else - assert(success, "supervisor configuration error: " .. error) + println("configuration error: " .. error) + return end 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..afdf6f3 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -77,7 +77,6 @@ function unit.new(reactor_id, num_boilers, num_turbines) tanks = {}, snas = {}, envd = {}, - sna_prod_rate = 0, -- redstone control io_ctl = nil, ---@type rs_controller valves = {}, ---@type unit_valves @@ -256,7 +255,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- PRIVATE FUNCTIONS -- - --#region time derivative utility functions + --#region Time Derivative Utility Functions -- compute a change with respect to time of the given value ---@param key string value key @@ -331,7 +330,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - --#region redstone I/O + --#region Redstone I/O -- create a generic valve interface ---@nodiscard @@ -398,8 +397,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) ---@class reactor_unit local public = {} - -- ADD/LINK DEVICES -- - --#region + --#region Add/Link Devices -- link the PLC ---@param plc_session plc_session_struct @@ -489,7 +487,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - -- UPDATE SESSION -- + --#region Update Session -- update (iterate) this unit function public.update() @@ -557,13 +555,15 @@ function unit.new(reactor_id, num_boilers, num_turbines) end end - -- AUTO CONTROL OPERATIONS -- - --#region + --#endregion + + --#region Auto Control Operations -- engage automatic control 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 +572,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 +583,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 +596,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 @@ -643,8 +644,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - -- OPERATIONS -- - --#region + --#region Operations -- queue a command to disable the reactor function public.disable() @@ -724,8 +724,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) --#endregion - -- READ STATES/PROPERTIES -- - --#region + --#region Read States/Properties -- check if an alarm of at least a certain priority level is tripped ---@nodiscard @@ -855,13 +854,15 @@ function unit.new(reactor_id, num_boilers, num_turbines) status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks } end - -- basic SNA statistical information - local total_peak = 0 + -- SNA statistical information + local total_peak, total_avail, total_out = 0, 0, 0 for i = 1, #self.snas do local db = self.snas[i].get_db() ---@type sna_session_db total_peak = total_peak + db.state.peak_production + total_avail = total_avail + db.state.production_rate + total_out = total_out + math.min(db.tanks.input.amount / 10, db.state.production_rate) end - status.sna = { #self.snas, public.get_sna_rate(), total_peak } + status.sna = { #self.snas, total_peak, total_avail, total_out } -- radiation monitors (environment detectors) status.envds = {} @@ -874,7 +875,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) return status end - -- get the current total [max] production rate is + -- get the current total max production rate ---@nodiscard ---@return number total_avail_rate function public.get_sna_rate() diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index e207c44..f43ff03 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -54,9 +54,7 @@ function logic.update_annunciator(self) -- variables for boiler, or reactor if no boilers used local total_boil_rate = 0.0 - ------------- - -- REACTOR -- - ------------- + --#region Reactor annunc.AutoControl = self.auto_engaged @@ -143,9 +141,9 @@ function logic.update_annunciator(self) self.plc_cache.ok = false end - --------------- - -- MISC RTUs -- - --------------- + --#endregion + + --#region Misc RTUs local max_rad, any_faulted = 0, false @@ -170,9 +168,9 @@ function logic.update_annunciator(self) end end - ------------- - -- BOILERS -- - ------------- + --#endregion + + --#region Boilers local boilers_ready = num_boilers == #self.boilers @@ -230,9 +228,9 @@ function logic.update_annunciator(self) boiler_water_dt_sum = _get_dt(DT_KEYS.ReactorCCool) end - --------------------------- - -- COOLANT FEED MISMATCH -- - --------------------------- + --#endregion + + --#region Coolant Feed Mismatch -- check coolant feed mismatch if using boilers, otherwise calculate with reactor local cfmismatch = false @@ -263,9 +261,9 @@ function logic.update_annunciator(self) annunc.CoolantFeedMismatch = cfmismatch - -------------- - -- TURBINES -- - -------------- + --#endregion + + --#region Turbines local turbines_ready = num_turbines == #self.turbines @@ -340,6 +338,8 @@ function logic.update_annunciator(self) annunc.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0 end + --#endregion + -- update auto control ready state for this unit self.db.control.ready = plc_ready and boilers_ready and turbines_ready end diff --git a/test/watch_psil_allocs.lua b/test/watch_psil_allocs.lua new file mode 100644 index 0000000..a45685e --- /dev/null +++ b/test/watch_psil_allocs.lua @@ -0,0 +1,56 @@ +-- add this to psil: + +--[[ + -- count the number of subscribers in this PSIL instance + ---@return integer count + function public.count() + local c = 0 + for _, val in pairs(ic) do + for _ = 1, #val.subscribers do c = c + 1 end + end + return c + end +]]-- + + +-- add this to coordinator iocontrol front panel heartbeat function: + +--[[ +if io.facility then + local count = io.facility.ps.count() + + count = count + io.facility.env_d_ps.count() + + for x = 1, #io.facility.induction_ps_tbl do + count = count + io.facility.induction_ps_tbl[x].count() + end + + for x = 1, #io.facility.sps_ps_tbl do + count = count + io.facility.sps_ps_tbl[x].count() + end + + for x = 1, #io.facility.tank_ps_tbl do + count = count + io.facility.tank_ps_tbl[x].count() + end + + for i = 1, #io.units do + local entry = io.units[i] ---@type ioctl_unit + + count = count + entry.unit_ps.count() + + for x = 1, #entry.boiler_ps_tbl do + count = count + entry.boiler_ps_tbl[x].count() + end + + for x = 1, #entry.turbine_ps_tbl do + count = count + entry.turbine_ps_tbl[x].count() + end + + for x = 1, #entry.tank_ps_tbl do + count = count + entry.tank_ps_tbl[x].count() + end + end + + log.debug(count) +end +]]--