Merge branch 'devel' into pocket-alpha-dev

This commit is contained in:
Mikayla 2024-03-07 16:21:37 +00:00
commit ce37a672a3
45 changed files with 2950 additions and 897 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: mikayla_f

View File

@ -5,12 +5,10 @@ on:
push: push:
branches: branches:
- main - main
- latest
- devel - devel
pull_request: pull_request:
branches: branches:
- main - main
- latest
- devel - devel
jobs: jobs:
check: check:

View File

@ -6,7 +6,6 @@ on:
push: push:
branches: branches:
- main - main
- latest
- devel - devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
@ -43,7 +42,7 @@ jobs:
- name: Create outputs folders - name: Create outputs folders
if: success() || failure() if: success() || failure()
shell: bash 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 - name: Generate manifest and shields for main branch
id: manifest-main id: manifest-main
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
@ -51,21 +50,6 @@ jobs:
- name: Save main's manifest - name: Save main's manifest
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }} if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
run: mv install_manifest.json deploy/manifests/main 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 # Generate manifest for devel branch
- name: Checkout devel - name: Checkout devel
id: checkout-devel id: checkout-devel

View File

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

127
ccmsi.lua
View File

@ -1,7 +1,7 @@
--[[ --[[
CC-MEK-SCADA Installer Utility 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 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, 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 println(message) print(tostring(message)) end
local function print(message) term.write(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 install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" 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 opts = { ... }
local mode, app, target 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 red() term.setTextColor(colors.red) end
local function orange() term.setTextColor(colors.orange) end local function orange() term.setTextColor(colors.orange) end
@ -59,17 +59,17 @@ local function ask_y_n(question, default)
end end
-- print out a white + blue text message -- 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 -- indicate actions to be taken based on package differences for installs/updates
local function show_pkg_change(name, v) local function show_pkg_change(name, v)
if v.v_local ~= nil then if v.v_local ~= nil then
if v.v_local ~= v.v_remote 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 elseif mode == "install" then
pkg_message("[" .. name .. "] reinstalling", v.v_local) pkg_message("["..name.."] reinstalling", v.v_local)
end 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 return v.v_local ~= v.v_remote
end end
@ -90,7 +90,7 @@ local function get_remote_manifest()
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)
if response == nil then if response == nil then
orange();println("Failed to get installation manifest from GitHub, cannot update or install.") 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, {} return false, {}
end end
@ -155,13 +155,13 @@ local function _clean_dir(dir, tree)
if tree == nil then tree = {} end if tree == nil then tree = {} end
local ls = fs.list(dir) local ls = fs.list(dir)
for _, val in pairs(ls) do for _, val in pairs(ls) do
local path = dir .. "/" .. val local path = dir.."/"..val
if fs.isDir(path) then if fs.isDir(path) then
_clean_dir(path, tree[val]) _clean_dir(path, tree[val])
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end 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 elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
fs.delete(path) fs.delete(path)
println("deleted " .. path) println("deleted "..path)
end end
end end
end end
@ -177,13 +177,13 @@ local function clean(manifest)
local ls = fs.list("/") local ls = fs.list("/")
for _, val in pairs(ls) do for _, val in pairs(ls) do
if fs.isDriveRoot(val) then if fs.isDriveRoot(val) then
yellow();println("skipped mount '" .. val .. "'") yellow();println("skipped mount '"..val.."'")
elseif fs.isDir(val) then elseif fs.isDir(val) then
if tree[val] ~= nil then lgray();_clean_dir("/" .. val, tree[val]) 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 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 #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 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
end end
@ -192,7 +192,7 @@ end
-- get and validate command line options -- 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 if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>") println("usage: ccmsi <mode> <app> <branch>")
@ -202,8 +202,8 @@ if #opts == 0 or opts[1] == "help" then
yellow() yellow()
println(" ccmsi check <branch> for target") println(" ccmsi check <branch> for target")
lgray() lgray()
println(" install - fresh install, overwrites config.lua") println(" install - fresh install")
println(" update - update files EXCEPT for config.lua") println(" update - update files")
println(" uninstall - delete files INCLUDING config/logs") println(" uninstall - delete files INCLUDING config/logs")
white();println("<app>");lgray() white();println("<app>");lgray()
println(" reactor-plc - reactor PLC firmware") println(" reactor-plc - reactor PLC firmware")
@ -213,7 +213,7 @@ if #opts == 0 or opts[1] == "help" then
println(" pocket - pocket application") println(" pocket - pocket application")
println(" installer - ccmsi installer (update only)") println(" installer - ccmsi installer (update only)")
white();println("<branch>") white();println("<branch>")
lgray();println(" main (default) | latest | devel");white() lgray();println(" main (default) | devel");white()
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
@ -233,14 +233,14 @@ else
-- determine target -- determine target
if mode == "check" then target = opts[2] else target = opts[3] end 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 if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main" target = "main"
end end
-- set paths -- set paths
install_manifest = manifest_path .. target .. "/install_manifest.json" install_manifest = manifest_path..target.."/install_manifest.json"
repo_path = repo_path .. target .. "/" repo_path = repo_path..target.."/"
end end
-- run selected mode -- run selected mode
@ -260,7 +260,7 @@ if mode == "check" then
-- list all versions -- list all versions
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple) 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 if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
blue();print(local_manifest.versions[key]) blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then 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 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 if update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua") 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 if dl == nil then
red();println("HTTP Error " .. err) red();println("HTTP Error "..err)
println("Installer download failed.");white() println("Installer download failed.");white()
else else
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location 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 ver.lockbox.v_remote = manifest.versions.lockbox
green() green()
if mode == "install" then if mode == "install" then print("Installing ") else print("Updating ") end
println("Installing " .. app .. " files...") println(app.." files...");white()
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()
ver.boot.changed = show_pkg_change("bootldr", ver.boot) ver.boot.changed = show_pkg_change("bootldr", ver.boot)
ver.common.changed = show_pkg_change("common", ver.common) 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 file_list = manifest.files
local size_list = manifest.sizes local size_list = manifest.sizes
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
local config_file = app .. "/config.lua"
table.insert(dependencies, app) table.insert(dependencies, app)
@ -421,15 +415,15 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
println("GET " .. file) println("GET "..file)
local dl, err = http.get(repo_path .. file) local dl, err = http.get(repo_path..file)
if dl == nil then if dl == nil then
red();println("HTTP Error " .. err) red();println("HTTP Error "..err)
success = false success = false
break break
else else
local handle = fs.open(install_dir .. "/" .. file, "w") local handle = fs.open(install_dir.."/"..file, "w")
handle.write(dl.readAll()) handle.write(dl.readAll())
handle.close() handle.close()
end end
@ -448,11 +442,9 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "install" or file ~= config_file then local temp_file = install_dir.."/"..file
local temp_file = install_dir .. "/" .. file if fs.exists(file) then fs.delete(file) end
if fs.exists(file) then fs.delete(file) end fs.move(temp_file, file)
fs.move(temp_file, file)
end
end end
end end
end end
@ -485,19 +477,17 @@ elseif mode == "install" or mode == "update" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "install" or file ~= config_file then println("GET "..file)
println("GET " .. file) local dl, err = http.get(repo_path..file)
local dl, err = http.get(repo_path .. file)
if dl == nil then if dl == nil then
red();println("HTTP Error " .. err) red();println("HTTP Error "..err)
success = false success = false
break break
else else
local handle = fs.open("/" .. file, "w") local handle = fs.open("/"..file, "w")
handle.write(dl.readAll()) handle.write(dl.readAll())
handle.close() handle.close()
end
end end
end end
end end
@ -527,11 +517,11 @@ elseif mode == "uninstall" then
end end
if manifest.versions[app] == nil then if manifest.versions[app] == nil then
red();println("Error: '" .. app .. "' is not installed.") red();println("Error: '"..app.."' is not installed.")
return return
end end
orange();println("Uninstalling all " .. app .. " files...") orange();println("Uninstalling all "..app.." files...")
-- ask for confirmation -- ask for confirmation
if not ask_y_n("Continue", false) then return end if not ask_y_n("Continue", false) then return end
@ -546,16 +536,16 @@ elseif mode == "uninstall" then
-- delete log file -- delete log file
local log_deleted = false local log_deleted = false
local settings_file = app .. ".settings" local settings_file = app..".settings"
local legacy_config_file = app .. "/config.lua" local legacy_config_file = app.."/config.lua"
lgray() lgray()
if fs.exists(legacy_config_file) then if fs.exists(legacy_config_file) then
log_deleted = pcall(function () log_deleted = pcall(function ()
local config = require(app .. ".config") local config = require(app..".config")
if fs.exists(config.LOG_PATH) then if fs.exists(config.LOG_PATH) then
fs.delete(config.LOG_PATH) fs.delete(config.LOG_PATH)
println("deleted log file " .. config.LOG_PATH) println("deleted log file "..config.LOG_PATH)
end end
end) end)
elseif fs.exists(settings_file) and settings.load(settings_file) then 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 if log ~= nil and fs.exists(log) then
log_deleted = true log_deleted = true
fs.delete(log) fs.delete(log)
println("deleted log file " .. log) println("deleted log file "..log)
end end
end end
@ -577,7 +567,7 @@ elseif mode == "uninstall" then
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do 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 end
local folder = files[1] local folder = files[1]
@ -588,13 +578,16 @@ elseif mode == "uninstall" then
if fs.isDir(folder) then if fs.isDir(folder) then
fs.delete(folder) fs.delete(folder)
println("deleted directory " .. folder) println("deleted directory "..folder)
end end
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 if fs.exists(settings_file) then
fs.delete(settings_file) fs.delete(settings_file);println("deleted "..settings_file)
println("deleted " .. settings_file)
end end
fs.delete("install_manifest.json") fs.delete("install_manifest.json")

View File

@ -1,15 +1,10 @@
print("CONFIGURE> SCANNING FOR CONFIGURATOR...") print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
if fs.exists("reactor-plc/configure.lua") then if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
require("reactor-plc.configure").configure() elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
elseif fs.exists("rtu/configure.lua") then elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
require("rtu.configure").configure() elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
elseif fs.exists("supervisor/configure.lua") then elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
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)")
else else
print("CONFIGURE> NO CONFIGURATOR FOUND") print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT") print("CONFIGURE> EXIT")

View File

@ -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

1322
coordinator/configure.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,11 +9,6 @@ local process = require("coordinator.process")
local apisessions = require("coordinator.session.apisessions") 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 PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
@ -26,182 +21,155 @@ local LINK_TIMEOUT = 60.0
local coordinator = {} local coordinator = {}
-- request the user to select a monitor ---@type crd_config
---@nodiscard local config = {}
---@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")
local iface = dialog.ask_options(names, "c") coordinator.config = config
if iface ~= false and iface ~= nil then -- load the coordinator configuration<br>
util.filter_table(names, function (x) return x ~= iface end) -- 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 end
return iface cfv.assert_type_int(config.LogMode)
end 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 ---@class monitors_struct
local monitors = { local monitors = {
primary = nil, ---@type table|nil main = nil, ---@type table|nil
primary_name = "", main_name = "",
flow = nil, ---@type table|nil flow = nil, ---@type table|nil
flow_name = "", flow_name = "",
unit_displays = {}, unit_displays = {},
unit_name_map = {} unit_name_map = {}
} }
local monitors_avail = ppm.get_monitor_list() local mon_cfv = util.new_validator()
local names = {}
local available = {}
-- get all interface names -- get all interface names
for iface, _ in pairs(monitors_avail) do local names = {}
table.insert(names, iface) for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
table.insert(available, iface)
end
-- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display) local function setup_monitors()
local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2) mon_cfv.assert_type_str(config.MainDisplay)
if #names < num_displays_needed then if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
local message = "not enough monitors connected (need " .. num_displays_needed .. ")" mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
println(message)
log.warning(message)
return false
end
-- attempt to load settings if mon_cfv.valid() then
if not settings.load("/coord.settings") then local w, h, _
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")
-- filter out already assigned monitors if not util.table_contains(names, config.MainDisplay) then
util.filter_table(available, function (x) return x ~= _primary end) return 2, "Main monitor is not connected."
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)
end end
if display == false then return false end monitors.main = ppm.get_periph(config.MainDisplay)
monitors.main_name = config.MainDisplay
unit_displays[i] = display monitors.main.setTextScale(0.5)
end w, _ = ppm.monitor_block_size(monitors.main.getSize())
else if w ~= 8 then
-- make sure all displays are connected return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
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
end end
while display == nil and #available > 0 do if not config.DisableFlowView then
display = ask_monitor(available) 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 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 monitors.unit_displays[i] = ppm.get_periph(display)
end 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 end
settings.set("UNIT_DISPLAYS", unit_displays) if cfv.valid() then
if not settings.save("/coord.settings") then local ok, result, message = pcall(setup_monitors)
log.warning("configure_monitors(): failed to save coordinator settings file") assert(ok, util.c("fatal error while trying to verify monitors: ", result))
end if result == 2 then return 2, message end
else return 1 end
for i = 1, #unit_displays do return 0, monitors
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
monitors.unit_name_map[i] = unit_displays[i]
end
return true, monitors
end end
-- dmesg print wrapper -- dmesg print wrapper
@ -246,13 +214,8 @@ end
---@nodiscard ---@nodiscard
---@param version string coordinator version ---@param version string coordinator version
---@param nic nic network interface device ---@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 ---@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 = { local self = {
sv_linked = false, sv_linked = false,
sv_addr = comms.BROADCAST, sv_addr = comms.BROADCAST,
@ -267,16 +230,16 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
est_task_done = nil est_task_done = nil
} }
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS --
-- configure network channels -- configure network channels
nic.closeAll() nic.closeAll()
nic.open(crd_channel) nic.open(config.CRD_Channel)
-- link nic to apisessions -- pass config to apisessions
apisessions.init(nic) apisessions.init(nic, config)
-- PRIVATE FUNCTIONS --
-- send a packet to the supervisor -- send a packet to the supervisor
---@param msg_type MGMT_TYPE|CRDN_TYPE ---@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) pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) 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 self.sv_seq_num = self.sv_seq_num + 1
end end
@ -310,7 +273,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack }) m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) 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 self.last_api_est_acks[packet.src_addr()] = ack
end end
@ -339,24 +302,24 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
if not self.sv_linked then if not self.sv_linked then
if self.est_tick_waiting == nil 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_last = self.est_start
self.est_tick_waiting, self.est_task_done = 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() _send_establish()
else 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 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) self.est_task_done(false)
if abort then if abort then
coordinator.log_comms("supervisor connection attempt cancelled by user") coordinator.log_comms("supervisor connection attempt cancelled by user")
elseif self.sv_config_err then 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 elseif not self.sv_linked then
if self.last_est_ack == ESTABLISH_ACK.DENY then if self.last_est_ack == ESTABLISH_ACK.DENY then
coordinator.log_comms("supervisor connection attempt denied") 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 ok = false
elseif self.sv_config_err then 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 ok = false
elseif (util.time_s() - self.est_last) > 1.0 then elseif (os.clock() - self.est_last) > 1.0 then
_send_establish() _send_establish()
self.est_last = util.time_s() self.est_last = os.clock()
end end
elseif self.est_tick_waiting ~= nil then elseif self.est_tick_waiting ~= nil then
self.est_task_done(true) self.est_task_done(true)
@ -405,10 +368,10 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
end end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
---@param config coord_auto_config configuration ---@param auto_cfg coord_auto_config configuration
function public.send_auto_start(config) function public.send_auto_start(auto_cfg)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { _send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
}) })
end 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 src_addr = packet.scada_frame.src_addr()
local protocol = packet.scada_frame.protocol() 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) 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 if not self.sv_linked then
log.debug("discarding pocket API packet before linked to supervisor") log.debug("discarding pocket API packet before linked to supervisor")
elseif protocol == PROTOCOL.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
@ -526,7 +489,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
else else
log.debug("illegal packet type " .. protocol .. " on pocket channel", true) log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
end end
elseif r_chan == svr_channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() 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 -- connection with supervisor established
if packet.length == 2 then if packet.length == 2 then
local est_ack = packet.data[1] local est_ack = packet.data[1]
local config = packet.data[2] local sv_config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
-- reset to disconnected before validating -- reset to disconnected before validating
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED) 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 -- get configuration
---@class facility_conf ---@class facility_conf
local conf = { local conf = {
num_units = config[1], ---@type integer num_units = sv_config[1], ---@type integer
cooling = config[2] ---@type sv_cooling_conf 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 -- init io controller
iocontrol.init(conf, public) iocontrol.init(conf, public, config.TempScale)
self.sv_addr = src_addr self.sv_addr = src_addr
self.sv_linked = true self.sv_linked = true

View File

@ -47,7 +47,23 @@ end
-- initialize the coordinator IO controller -- initialize the coordinator IO controller
---@param conf facility_conf configuration ---@param conf facility_conf configuration
---@param comms coord_comms comms reference ---@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 -- facility data structure
---@class ioctl_facility ---@class ioctl_facility
io.facility = { io.facility = {
@ -219,7 +235,10 @@ function iocontrol.init(conf, comms)
control_state = false, control_state = false,
burn_rate_cmd = 0.0, burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(), 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_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM, waste_product = types.WASTE_PRODUCT.PLUTONIUM,
@ -923,7 +942,7 @@ function iocontrol.update_unit_statuses(statuses)
local boil_sum = 0 local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[i] == nil then if rtu_statuses.boilers[id] == nil then
-- disconnected -- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1) unit.boiler_ps_tbl[id].publish("computed_status", 1)
end end
@ -966,7 +985,7 @@ function iocontrol.update_unit_statuses(statuses)
local flow_sum = 0 local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then if rtu_statuses.turbines[id] == nil then
-- disconnected -- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1) unit.turbine_ps_tbl[id].publish("computed_status", 1)
end end
@ -1009,7 +1028,7 @@ function iocontrol.update_unit_statuses(statuses)
-- dynamic tank statuses -- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then if type(rtu_statuses.tanks) == "table" then
for id = 1, #unit.tank_ps_tbl do for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[i] == nil then if rtu_statuses.tanks[id] == nil then
-- disconnected -- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1) unit.tank_ps_tbl[id].publish("computed_status", 1)
end end
@ -1048,12 +1067,14 @@ function iocontrol.update_unit_statuses(statuses)
-- solar neutron activator status info -- solar neutron activator status info
if type(rtu_statuses.sna) == "table" then if type(rtu_statuses.sna) == "table" then
unit.num_snas = rtu_statuses.sna[1] ---@type integer 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[2] ---@type number
unit.sna_peak_rate = rtu_statuses.sna[3] ---@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_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_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 sna_count_sum = sna_count_sum + unit.num_snas
else else
@ -1201,7 +1222,7 @@ function iocontrol.update_unit_statuses(statuses)
local u_spent_rate = waste_rate local u_spent_rate = waste_rate
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0) 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("pu_rate", u_pu_rate)
unit.unit_ps.publish("po_rate", u_po_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)) unit.unit_ps.publish("sna_in", util.trinary(is_pu, 0, burn_rate))
if unit.waste_product == types.WASTE_PRODUCT.POLONIUM then 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_pl_rate", u_po_rate)
unit.unit_ps.publish("po_am_rate", 0) unit.unit_ps.publish("po_am_rate", 0)
po_pl_rate = po_pl_rate + u_po_rate po_pl_rate = po_pl_rate + u_po_rate
elseif unit.waste_product == types.WASTE_PRODUCT.ANTI_MATTER then 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_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", u_po_rate) unit.unit_ps.publish("po_am_rate", u_po_rate)
po_am_rate = po_am_rate + u_po_rate po_am_rate = po_am_rate + u_po_rate
u_spent_rate = 0
else else
unit.unit_ps.publish("po_pl_rate", 0) unit.unit_ps.publish("po_pl_rate", 0)
unit.unit_ps.publish("po_am_rate", 0) unit.unit_ps.publish("po_am_rate", 0)

View File

@ -19,15 +19,20 @@ local process = {}
local self = { local self = {
io = nil, ---@type ioctl io = nil, ---@type ioctl
comms = nil, ---@type coord_comms comms = nil, ---@type coord_comms
---@class coord_auto_config ---@class coord_control_states
config = { control_states = {
mode = PROCESS.INACTIVE, ---@class coord_auto_config
burn_target = 0.0, process = {
charge_target = 0.0, mode = PROCESS.INACTIVE,
gen_target = 0.0, burn_target = 0.0,
limits = {}, charge_target = 0.0,
waste_product = PRODUCT.PLUTONIUM, gen_target = 0.0,
pu_fallback = false 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.io = iocontrol
self.comms = coord_comms self.comms = coord_comms
local ctl_proc = self.control_states.process
for i = 1, self.io.facility.num_units do for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1 ctl_proc.limits[i] = 0.1
end end
-- load settings local ctrl_states = settings.get("ControlStates", {})
if not settings.load("/coord.settings") then local config = ctrl_states.process ---@type coord_auto_config
log.error("process.init(): failed to load coordinator settings file")
end
-- facility auto control configuration -- facility auto control configuration
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
if type(config) == "table" then if type(config) == "table" then
self.config.mode = config.mode ctl_proc.mode = config.mode
self.config.burn_target = config.burn_target ctl_proc.burn_target = config.burn_target
self.config.charge_target = config.charge_target ctl_proc.charge_target = config.charge_target
self.config.gen_target = config.gen_target ctl_proc.gen_target = config.gen_target
self.config.limits = config.limits ctl_proc.limits = config.limits
self.config.waste_product = config.waste_product ctl_proc.waste_product = config.waste_product
self.config.pu_fallback = config.pu_fallback ctl_proc.pu_fallback = config.pu_fallback
self.io.facility.ps.publish("process_mode", self.config.mode) self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
self.io.facility.ps.publish("process_waste_product", self.config.waste_product) self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback) 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 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 end
log.info("PROCESS: loaded auto control settings from coord.settings") log.info("PROCESS: loaded auto control settings")
-- notify supervisor of auto waste config -- 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_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback) self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
end end
-- unit waste states -- 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 if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do 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) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
end end
log.info("PROCESS: loaded unit waste mode settings from coord.settings") log.info("PROCESS: loaded unit waste mode settings")
end end
-- unit priority groups -- 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 if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do 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) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
end end
log.info("PROCESS: loaded priority groups settings from coord.settings") log.info("PROCESS: loaded priority groups settings")
end end
end end
@ -155,15 +161,10 @@ function process.set_unit_waste(id, mode)
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", 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 if not settings.save("/coordinator.settings") then
waste_mode[id] = mode
settings.set("WASTE_MODES", waste_mode)
if not settings.save("/coord.settings") then
log.error("process.set_unit_waste(): failed to save coordinator settings file") log.error("process.set_unit_waste(): failed to save coordinator settings file")
end end
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) 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)) 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 if not settings.save("/coordinator.settings") then
prio_groups[unit_id] = group_id
settings.set("PRIORITY_GROUPS", prio_groups)
if not settings.save("/coord.settings") then
log.error("process.set_group(): failed to save coordinator settings file") log.error("process.set_group(): failed to save coordinator settings file")
end end
end end
@ -217,20 +213,14 @@ end
-- write auto process control to config file -- write auto process control to config file
local function _write_auto_config() 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 -- save config
settings.set("PROCESS", self.config) settings.set("ControlStates", self.control_states)
local saved = settings.save("/coord.settings") local saved = settings.save("/coordinator.settings")
if not saved then if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file") log.warning("process._write_auto_config(): failed to save coordinator settings file")
end end
return not not saved return saved
end end
-- stop automatic process control -- stop automatic process control
@ -241,7 +231,7 @@ end
-- start automatic process control -- start automatic process control
function process.start_auto() 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") log.debug("PROCESS: START AUTO CTL")
end end
@ -253,7 +243,7 @@ function process.set_process_waste(product)
log.debug(util.c("PROCESS: SET WASTE ", product)) log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save -- update config table and save
self.config.waste_product = product self.control_states.process.waste_product = product
_write_auto_config() _write_auto_config()
end end
@ -265,7 +255,7 @@ function process.set_pu_fallback(enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled)) log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save -- update config table and save
self.config.pu_fallback = enabled self.control_states.process.pu_fallback = enabled
_write_auto_config() _write_auto_config()
end end
@ -279,11 +269,12 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE") log.debug("PROCESS: SAVE")
-- update config table -- update config table
self.config.mode = mode local ctl_proc = self.control_states.process
self.config.burn_target = burn_target ctl_proc.mode = mode
self.config.charge_target = charge_target ctl_proc.burn_target = burn_target
self.config.gen_target = gen_target ctl_proc.charge_target = charge_target
self.config.limits = limits ctl_proc.gen_target = gen_target
ctl_proc.limits = limits
-- save config -- save config
self.io.facility.save_cfg_ack(_write_auto_config()) self.io.facility.save_cfg_ack(_write_auto_config())
@ -294,22 +285,23 @@ end
function process.start_ack_handle(response) function process.start_ack_handle(response)
local ack = response[1] local ack = response[1]
self.config.mode = response[2] local ctl_proc = self.control_states.process
self.config.burn_target = response[3] ctl_proc.mode = response[2]
self.config.charge_target = response[4] ctl_proc.burn_target = response[3]
self.config.gen_target = response[5] 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 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 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 end
self.io.facility.ps.publish("process_mode", self.config.mode) self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
self.io.facility.start_ack(ack) self.io.facility.start_ack(ack)
end end
@ -317,14 +309,14 @@ end
-- record waste product state after attempting to change it -- record waste product state after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state ---@param response WASTE_PRODUCT supervisor waste product state
function process.waste_ack_handle(response) 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) self.io.facility.ps.publish("process_waste_product", response)
end end
-- record plutonium fallback state after attempting to change it -- record plutonium fallback state after attempting to change it
---@param response boolean supervisor plutonium fallback state ---@param response boolean supervisor plutonium fallback state
function process.pu_fb_ack_handle(response) 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) self.io.facility.ps.publish("process_pu_fallback", response)
end end

View File

@ -3,7 +3,6 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
@ -53,6 +52,16 @@ local function _init_display(monitor)
end end
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 -- disable the flow view
---@param disable boolean ---@param disable boolean
function renderer.legacy_disable_flow_view(disable) function renderer.legacy_disable_flow_view(disable)
@ -65,15 +74,15 @@ function renderer.set_displays(monitors)
engine.monitors = monitors engine.monitors = monitors
-- report to front panel as connected -- 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) iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
end end
-- init all displays in use by the renderer -- init all displays in use by the renderer
function renderer.init_displays() function renderer.init_displays()
-- init primary and flow monitors -- init main and flow monitors
_init_display(engine.monitors.primary) _init_display(engine.monitors.main)
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
-- init unit displays -- init unit displays
@ -93,43 +102,10 @@ function renderer.init_displays()
end end
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 -- initialize the dmesg output window
function renderer.init_dmesg() function renderer.init_dmesg()
local disp_x, disp_y = engine.monitors.primary.getSize() local disp_w, disp_h = engine.monitors.main.getSize()
engine.dmesg_window = window.create(engine.monitors.primary, 1, 1, disp_x, disp_y) engine.dmesg_window = window.create(engine.monitors.main, 1, 1, disp_w, disp_h)
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end end
@ -200,8 +176,8 @@ function renderer.try_start_ui()
status, msg = pcall(function () status, msg = pcall(function ()
-- show main view on main monitor -- show main view on main monitor
if engine.monitors.primary ~= nil then if engine.monitors.main ~= nil then
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
main_view(engine.ui.main_display) main_view(engine.ui.main_display)
end end
@ -276,43 +252,43 @@ function renderer.ui_ready() return engine.ui_ready end
function renderer.handle_disconnect(device) function renderer.handle_disconnect(device)
local is_used = false local is_used = false
if engine.monitors ~= nil then if not engine.monitors then return false end
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
is_used = true if engine.monitors.main == device then
engine.monitors.primary = nil if engine.ui.main_display ~= nil then
engine.ui.main_display = nil -- delete element tree and clear root UI elements
engine.ui.main_display.delete()
end
iocontrol.fp_monitor_state("main", false) is_used = true
elseif engine.monitors.flow == device then engine.monitors.main = nil
if engine.ui.flow_display ~= nil then engine.ui.main_display = nil
-- delete element tree and clear root UI elements
engine.ui.flow_display.delete()
end
is_used = true iocontrol.fp_monitor_state("main", false)
engine.monitors.flow = nil elseif engine.monitors.flow == device then
engine.ui.flow_display = nil 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) is_used = true
else engine.monitors.flow = nil
for idx, monitor in pairs(engine.monitors.unit_displays) do engine.ui.flow_display = nil
if monitor == device then
if engine.ui.unit_displays[idx] ~= nil then
engine.ui.unit_displays[idx].delete()
end
is_used = true iocontrol.fp_monitor_state("flow", false)
engine.monitors.unit_displays[idx] = nil else
engine.ui.unit_displays[idx] = nil for idx, monitor in pairs(engine.monitors.unit_displays) do
if monitor == device then
iocontrol.fp_monitor_state(idx, false) if engine.ui.unit_displays[idx] ~= nil then
break engine.ui.unit_displays[idx].delete()
end 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 end
end end
@ -327,52 +303,29 @@ end
function renderer.handle_reconnect(name, device) function renderer.handle_reconnect(name, device)
local is_used = false local is_used = false
if engine.monitors ~= nil then if not engine.monitors then return false end
if engine.monitors.primary_name == name then
is_used = true
_init_display(device)
engine.monitors.primary = device
local disp_x, disp_y = engine.monitors.primary.getSize() -- note: handle_resize is a more adaptive way of re-initializing a connected monitor
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary) -- 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 if engine.monitors.main_name == name then
engine.dmesg_window.setVisible(false) is_used = true
engine.monitors.main = device
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root} renderer.handle_resize(name)
main_view(engine.ui.main_display) elseif engine.monitors.flow_name == name then
else is_used = true
engine.dmesg_window.setVisible(true) engine.monitors.flow = device
engine.dmesg_window.redraw()
end
iocontrol.fp_monitor_state("main", true) renderer.handle_resize(name)
elseif engine.monitors.flow_name == name then else
is_used = true for idx, monitor in ipairs(engine.monitors.unit_name_map) do
_init_display(device) if monitor == name then
engine.monitors.flow = device is_used = true
engine.monitors.unit_displays[idx] = device
if engine.ui_ready and (engine.ui.flow_display == nil) then renderer.handle_resize(name)
engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root} break
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
end end
end end
end end
@ -380,6 +333,137 @@ function renderer.handle_reconnect(name, device)
return is_used return is_used
end end
-- handle a monitor being resized<br>
-- 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 -- handle a touch event
---@param event mouse_interaction|nil ---@param event mouse_interaction|nil
@ -388,16 +472,15 @@ function renderer.handle_mouse(event)
if engine.fp_ready and event.monitor == "terminal" then if engine.fp_ready and event.monitor == "terminal" then
engine.ui.front_panel.handle_mouse(event) engine.ui.front_panel.handle_mouse(event)
elseif engine.ui_ready then elseif engine.ui_ready then
if event.monitor == engine.monitors.primary_name then if event.monitor == engine.monitors.main_name then
engine.ui.main_display.handle_mouse(event) if engine.ui.main_display then engine.ui.main_display.handle_mouse(event) end
elseif event.monitor == engine.monitors.flow_name then 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 else
for id, monitor in ipairs(engine.monitors.unit_name_map) do for id, monitor in ipairs(engine.monitors.unit_name_map) do
if event.monitor == monitor then local display = engine.ui.unit_displays[id]
local layout = engine.ui.unit_displays[id] ---@type graphics_element if event.monitor == monitor and display then
layout.handle_mouse(event) if display then display.handle_mouse(event) end
break
end end
end end
end end

View File

@ -3,7 +3,6 @@ local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("coordinator.config")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local pocket = require("coordinator.session.pocket") local pocket = require("coordinator.session.pocket")
@ -11,7 +10,8 @@ local pocket = require("coordinator.session.pocket")
local apisessions = {} local apisessions = {}
local self = { local self = {
nic = nil, nic = nil, ---@type nic
config = nil, ---@type crd_config
next_id = 0, next_id = 0,
sessions = {} sessions = {}
} }
@ -32,7 +32,7 @@ local function _api_handle_outq(session)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then if msg.qtype == mqueue.TYPE.PACKET then
-- handle a packet to be sent -- 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 elseif msg.qtype == mqueue.TYPE.COMMAND then
-- handle instruction/notification -- handle instruction/notification
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@ -59,7 +59,7 @@ local function _shutdown(session)
while session.out_queue.ready() do while session.out_queue.ready() do
local msg = session.out_queue.pop() local msg = session.out_queue.pop()
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then 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
end end
@ -69,9 +69,11 @@ end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- initialize apisessions -- initialize apisessions
---@param nic nic ---@param nic nic network interface
function apisessions.init(nic) ---@param config crd_config coordinator config
function apisessions.init(nic, config)
self.nic = nic self.nic = nic
self.config = config
end end
-- find a session by remote port -- find a session by remote port
@ -103,7 +105,7 @@ function apisessions.establish_session(source_addr, version)
local id = self.next_id 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) table.insert(self.sessions, pkt_s)
local mt = { local mt = {

View File

@ -14,7 +14,7 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("coordinator.config") local configure = require("coordinator.configure")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
@ -22,7 +22,9 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -34,32 +36,66 @@ local log_comms = coordinator.log_comms
local log_crypto = coordinator.log_crypto 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) local wait_on_load = true
cfv.assert_channel(config.CRD_CHANNEL) local loaded, monitors = coordinator.load_config()
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)
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
---------------------------------------- ----------------------------------------
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true) log.init(config.LogPath, config.LogMode, config.LogDebug)
log.info("========================================") log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
@ -77,39 +113,16 @@ local function main()
-- system startup -- system startup
---------------------------------------- ----------------------------------------
-- mount connected devices -- log mounts now since mounting was done before logging was ready
ppm.mount_all() ppm.log_mounts()
-- report versions/init fp PSIL -- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version) 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 -- init renderer
renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true) renderer.legacy_disable_flow_view(config.DisableFlowView)
renderer.set_displays(monitors) renderer.set_displays(monitors)
renderer.init_displays() 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() renderer.init_dmesg()
-- lets get started! -- lets get started!
@ -132,7 +145,7 @@ local function main()
else else
local sounder_start = util.time_ms() local sounder_start = util.time_ms()
log_boot("annunciator alarm speaker connected") 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_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured") log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true) iocontrol.fp_has_speaker(true)
@ -143,8 +156,8 @@ local function main()
---------------------------------------- ----------------------------------------
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
local init_time = network.init_mac(config.AUTH_KEY) local init_time = network.init_mac(config.AuthKey)
log_crypto("HMAC init took " .. init_time .. "ms") log_crypto("HMAC init took " .. init_time .. "ms")
end end
@ -161,14 +174,13 @@ local function main()
end end
-- create connection watchdog -- create connection watchdog
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT) local conn_watchdog = util.new_watchdog(config.SVR_Timeout)
conn_watchdog.cancel() conn_watchdog.cancel()
log.debug("startup> conn watchdog created") log.debug("startup> conn watchdog created")
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) local nic = network.nic(modem)
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL, local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog)
config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
log.debug("startup> comms init") log.debug("startup> comms init")
log_comms("comms initialized") log_comms("comms initialized")
@ -214,7 +226,7 @@ local function main()
local link_failed = false local link_failed = false
local ui_ok = true 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 -- start clock
loop_clock.start() loop_clock.start()
@ -291,6 +303,11 @@ local function main()
iocontrol.fp_has_speaker(true) iocontrol.fp_has_speaker(true)
end end
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 elseif event == "timer" then
if loop_clock.is_clock(param1) then if loop_clock.is_clock(param1) then
-- main loop tick -- main loop tick

View File

@ -1,5 +1,7 @@
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local iocontrol = require("coordinator.iocontrol")
local core = require("graphics.core") local core = require("graphics.core")
local Rectangle = require("graphics.elements.rectangle") local Rectangle = require("graphics.elements.rectangle")
@ -13,6 +15,7 @@ local cpair = core.cpair
local border = core.border local border = core.border
local text_fg_bg = style.text_colors local text_fg_bg = style.text_colors
local lu_col = style.lu_colors
-- new boiler view -- new boiler view
---@param root graphics_element parent ---@param root graphics_element parent
@ -20,14 +23,16 @@ local text_fg_bg = style.text_colors
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) 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 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 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 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=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",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) 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) 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} TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}

View File

@ -1,5 +1,7 @@
local types = require("scada-common.types") local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
@ -23,15 +25,17 @@ local lu_col = style.lu_colors
---@param y integer top left y ---@param y integer top left y
---@param ps psil ps interface ---@param ps psil ps interface
local function new_view(root, x, y, ps) 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 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 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 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} 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) 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) burn_r.register(ps, "act_burn_rate", burn_r.update)
heating_r.register(ps, "heating_rate", heating_r.update) heating_r.register(ps, "heating_rate", heating_r.update)

View File

@ -3,6 +3,7 @@
-- --
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
@ -51,8 +52,9 @@ local period = core.flasher.PERIOD
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer ---@param id integer
local function init(parent, id) local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit local db = iocontrol.get_db()
local f_ps = iocontrol.get_db().facility.ps local unit = db.units[id] ---@type ioctl_unit
local f_ps = db.facility.ps
local main = Div{parent=parent,x=1,y=1} local main = Div{parent=parent,x=1,y=1}
@ -114,8 +116,9 @@ local function init(parent, id)
end) end)
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label} 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} local fmt = util.trinary(string.len(db.temp_label) == 2, "%10.2f", "%11.2f")
core_temp.register(u_ps, "temp", core_temp.update) 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} 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} 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}

View File

@ -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 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 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 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.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.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.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.2f",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 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) waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
pu_rate.register(unit.unit_ps, "pu_rate", pu_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_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_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.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) sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root return root

View File

@ -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

View File

@ -348,7 +348,7 @@ local function init(main)
status.register(facility.sps_ps_tbl[1], "computed_status", status.update) 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} 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) 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} 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 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 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 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.3f",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) pu.register(facility.ps, "pu_rate", pu.update)
po.register(facility.ps, "po_rate", po.update) po.register(facility.ps, "po_rate", po.update)

View File

@ -845,9 +845,12 @@ function element.new(args, child_offset_x, child_offset_y)
-- re-draw this element and all its children -- re-draw this element and all its children
function public.redraw() function public.redraw()
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
protected.window.setBackgroundColor(protected.fg_bg.bkg) protected.window.setBackgroundColor(protected.fg_bg.bkg)
protected.window.setTextColor(protected.fg_bg.fgd) protected.window.setTextColor(protected.fg_bg.fgd)
protected.window.clear() protected.window.clear()
protected.window.setBackgroundColor(bg)
protected.window.setTextColor(fg)
protected.redraw() protected.redraw()
for _, child in pairs(protected.children) do child.get().redraw() end for _, child in pairs(protected.children) do child.get().redraw() end
end end

View File

@ -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

578
pocket/configure.lua Normal file
View File

@ -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 = "<not set>" 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

View File

@ -13,17 +13,57 @@ local LINK_STATE = iocontrol.LINK_STATE
local pocket = {} 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 -- pocket coordinator + supervisor communications
---@nodiscard ---@nodiscard
---@param version string pocket version ---@param version string pocket version
---@param nic nic network interface device ---@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 sv_watchdog watchdog
---@param api_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 = { local self = {
sv = { sv = {
linked = false, linked = false,
@ -42,13 +82,13 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
establish_delay_counter = 0 establish_delay_counter = 0
} }
comms.set_trusted_range(range) comms.set_trusted_range(config.TrustedRange)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure network channels -- configure network channels
nic.closeAll() nic.closeAll()
nic.open(pkt_channel) nic.open(config.PKT_Channel)
-- send a management packet to the supervisor -- send a management packet to the supervisor
---@param msg_type MGMT_TYPE ---@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) pkt.make(msg_type, msg)
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) 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 self.sv.seq_num = self.sv.seq_num + 1
end end
@ -74,7 +114,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) 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 self.api.seq_num = self.api.seq_num + 1
end end
@ -217,9 +257,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
local src_addr = packet.scada_frame.src_addr() 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) 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 -- check sequence number
if self.api.r_seq_num == nil then if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num() 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 else
log.debug("illegal packet type " .. protocol .. " from coordinator", true) log.debug("illegal packet type " .. protocol .. " from coordinator", true)
end end
elseif r_chan == svr_channel then elseif r_chan == config.SVR_Channel then
-- check sequence number -- check sequence number
if self.sv.r_seq_num == nil then if self.sv.r_seq_num == nil then
self.sv.r_seq_num = packet.scada_frame.seq_num() self.sv.r_seq_num = packet.scada_frame.seq_num()

View File

@ -13,38 +13,41 @@ local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local config = require("pocket.config") local configure = require("pocket.configure")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") 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 = util.println
local println_ts = util.println_ts 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) local config = pocket.config
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")
---------------------------------------- ----------------------------------------
-- log init -- 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("========================================")
log.info("BOOTING pocket.startup " .. POCKET_VERSION) log.info("BOOTING pocket.startup " .. POCKET_VERSION)
@ -69,8 +72,8 @@ local function main()
---------------------------------------- ----------------------------------------
-- message authentication init -- message authentication init
if type(config.AUTH_KEY) == "string" then if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
network.init_mac(config.AUTH_KEY) network.init_mac(config.AuthKey)
end end
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED) iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
@ -85,8 +88,8 @@ local function main()
-- create connection watchdogs -- create connection watchdogs
local conn_wd = { local conn_wd = {
sv = util.new_watchdog(config.COMMS_TIMEOUT), sv = util.new_watchdog(config.ConnTimeout),
api = util.new_watchdog(config.COMMS_TIMEOUT) api = util.new_watchdog(config.ConnTimeout)
} }
conn_wd.sv.cancel() conn_wd.sv.cancel()
@ -96,8 +99,7 @@ local function main()
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) local nic = network.nic(modem)
local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL, local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api)
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
log.debug("startup> comms init") log.debug("startup> comms init")
-- base loop clock (2Hz, 10 ticks) -- base loop clock (2Hz, 10 ticks)

View File

@ -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}} 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 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=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} 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_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49} local plc_c_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=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} 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_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_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=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} 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} 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=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} 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_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_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)} 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() local function back_from_settings()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then 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() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end 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(settings_cfg, true)
load_settings(ini_cfg) 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=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)} 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} 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} 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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
@ -646,7 +654,7 @@ local function reset_term()
end end
-- run the reactor PLC configurator -- 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) function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true tool_ctl.ask_config = ask_config == true

View File

@ -70,9 +70,9 @@ function databus.tx_link_state(state)
end end
-- transmit reactor enable state across the bus -- transmit reactor enable state across the bus
---@param active boolean reactor active ---@param active any reactor active
function databus.tx_reactor_state(active) function databus.tx_reactor_state(active)
databus.ps.publish("reactor_active", active) databus.ps.publish("reactor_active", active == true)
end end
-- transmit RPS data across the bus -- transmit RPS data across the bus

View File

@ -37,14 +37,17 @@ function plc.load_config()
config.Networked = settings.get("Networked") config.Networked = settings.get("Networked")
config.UnitID = settings.get("UnitID") config.UnitID = settings.get("UnitID")
config.EmerCoolEnable = settings.get("EmerCoolEnable") config.EmerCoolEnable = settings.get("EmerCoolEnable")
config.EmerCoolSide = settings.get("EmerCoolSide") config.EmerCoolSide = settings.get("EmerCoolSide")
config.EmerCoolColor = settings.get("EmerCoolColor") config.EmerCoolColor = settings.get("EmerCoolColor")
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel") config.PLC_Channel = settings.get("PLC_Channel")
config.ConnTimeout = settings.get("ConnTimeout") config.ConnTimeout = settings.get("ConnTimeout")
config.TrustedRange = settings.get("TrustedRange") config.TrustedRange = settings.get("TrustedRange")
config.AuthKey = settings.get("AuthKey") config.AuthKey = settings.get("AuthKey")
config.LogMode = settings.get("LogMode") config.LogMode = settings.get("LogMode")
config.LogPath = settings.get("LogPath") config.LogPath = settings.get("LogPath")
config.LogDebug = settings.get("LogDebug") config.LogDebug = settings.get("LogDebug")
@ -71,6 +74,7 @@ function plc.load_config()
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath) cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug) cfv.assert_type_bool(config.LogDebug)
@ -125,6 +129,21 @@ function plc.rps_init(reactor, is_formed)
end end
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) -- set emergency coolant control (if configured)
---@param state boolean true to enable emergency coolant, false to disable ---@param state boolean true to enable emergency coolant, false to disable
local function _set_emer_cool(state) local function _set_emer_cool(state)
@ -163,25 +182,20 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is formed -- check if the reactor is formed
local function _is_formed() local function _is_formed()
local formed = reactor.isFormed() local formed = reactor.isFormed()
if formed == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(formed) then
-- lost the peripheral or terminated, handled later
_set_fault()
else
self.formed = formed self.formed = formed
end
if not self.state[state_keys.sys_fail] then -- always update, since some ppm failures constitute not being formed
self.state[state_keys.sys_fail] = not formed if not self.state[state_keys.sys_fail] then
end self.state[state_keys.sys_fail] = not self.formed
end end
end end
-- check if the reactor is force disabled -- check if the reactor is force disabled
local function _is_force_disabled() local function _is_force_disabled()
local disabled = reactor.isForceDisabled() local disabled = reactor.isForceDisabled()
if disabled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(disabled) then
-- lost the peripheral or terminated, handled later
_set_fault()
else
self.force_disabled = disabled self.force_disabled = disabled
if not self.state[state_keys.force_disabled] then if not self.state[state_keys.force_disabled] then
@ -193,22 +207,16 @@ function plc.rps_init(reactor, is_formed)
-- check for high damage -- check for high damage
local function _high_damage() local function _high_damage()
local damage_percent = reactor.getDamagePercent() local damage_percent = reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(damage_percent) and not self.state[state_keys.high_dmg] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_dmg] then
self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT self.state[state_keys.high_dmg] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT
end end
end end
-- check if the reactor is at a critically high temperature -- check if the reactor is at a critically high temperature
local function _high_temp() local function _high_temp()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 -- mekanism: MAX_DAMAGE_TEMPERATURE = 1200K
local temp = reactor.getTemperature() local temp = reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(temp) and not self.state[state_keys.high_temp] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_temp] then
self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE
end end
end end
@ -216,10 +224,7 @@ function plc.rps_init(reactor, is_formed)
-- check if there is very low coolant -- check if there is very low coolant
local function _low_coolant() local function _low_coolant()
local coolant_filled = reactor.getCoolantFilledPercentage() local coolant_filled = reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(coolant_filled) and not self.state[state_keys.low_coolant] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.low_coolant] then
self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL self.state[state_keys.low_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL
end end
end end
@ -227,10 +232,7 @@ function plc.rps_init(reactor, is_formed)
-- check for excess waste (>80% filled) -- check for excess waste (>80% filled)
local function _excess_waste() local function _excess_waste()
local w_filled = reactor.getWasteFilledPercentage() local w_filled = reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(w_filled) and not self.state[state_keys.ex_waste] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_waste] then
self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL
end end
end end
@ -238,10 +240,7 @@ function plc.rps_init(reactor, is_formed)
-- check for heated coolant backup (>95% filled) -- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant() local function _excess_heated_coolant()
local hc_filled = reactor.getHeatedCoolantFilledPercentage() local hc_filled = reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(hc_filled) and not self.state[state_keys.ex_hcoolant] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_hcoolant] then
self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL
end end
end end
@ -249,10 +248,7 @@ function plc.rps_init(reactor, is_formed)
-- check if there is no fuel -- check if there is no fuel
local function _insufficient_fuel() local function _insufficient_fuel()
local fuel = reactor.getFuelFilledPercentage() local fuel = reactor.getFuelFilledPercentage()
if fuel == ppm.ACCESS_FAULT then if _check_and_handle_ppm_call(fuel) and not self.state[state_keys.no_fuel] then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.no_fuel] then
self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL
end end
end end
@ -473,13 +469,22 @@ function plc.rps_init(reactor, is_formed)
self.tripped = false self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK self.trip_cause = RPS_TRIP_CAUSE.OK
for i = 1, #self.state do for i = 1, #self.state do self.state[i] = false end
self.state[i] = false
end
if not quiet then log.info("RPS: reset") end if not quiet then log.info("RPS: reset") end
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 -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
function public.auto_reset() function public.auto_reset()
self.state[state_keys.automatic] = false self.state[state_keys.automatic] = false

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -31,9 +31,13 @@ if not plc.load_config() then
-- try to reconfigure (user action) -- try to reconfigure (user action)
local success, error = configure.configure(true) local success, error = configure.configure(true)
if success then 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 else
assert(success, "reactor PLC configuration error: " .. error) println("configuration error: " .. error)
return
end end
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 -- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then 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") log.warning("init> no reactor on startup")
plc_state.init_ok = false plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
elseif not smem_dev.reactor.isFormed() then 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") 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.degraded = true
plc_state.reactor_formed = false plc_state.reactor_formed = false
end end

View File

@ -125,9 +125,8 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor) plc_comms.reconnect_reactor(plc_dev.reactor)
end end
-- reset RPS for newly connected reactor -- partial reset of RPS, specific to becoming formed
-- without this, is_formed will be out of date and cause it to think its no longer formed again rps.reset_formed()
rps.reset()
else else
-- fully lost the reactor now :( -- fully lost the reactor now :(
println_ts("reactor lost (failed reconnect)!") println_ts("reactor lost (failed reconnect)!")
@ -231,9 +230,8 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor) plc_comms.reconnect_reactor(plc_dev.reactor)
end end
-- reset RPS for newly connected reactor -- partial reset of RPS, specific to becoming formed
-- without this, is_formed will be out of date and cause it to think its no longer formed again rps.reset_formed()
rps.reset()
end end
elseif networked and type == "modem" then elseif networked and type == "modem" then
-- note, check init_ok first since nic will be nil if it is false -- note, check init_ok first since nic will be nil if it is false

View File

@ -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}} 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 local y_start = 2
@ -324,7 +324,7 @@ local function config_view(display)
--#endregion --#endregion
--#region SPEAKER CONFIG --#region Speakers
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49} local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
@ -353,7 +353,7 @@ local function config_view(display)
--#endregion --#endregion
--#region NET CONFIG --#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49} 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_2 = Div{parent=net_cfg,x=2,y=4,width=49}
@ -455,7 +455,7 @@ local function config_view(display)
--#endregion --#endregion
--#region LOG CONFIG --#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49} local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
@ -494,7 +494,7 @@ local function config_view(display)
--#endregion --#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_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_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)} 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() local function back_from_settings()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then 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("Peripherals") == nil then settings.set("Peripherals", {}) end
if settings.get("Redstone") == nil then settings.set("Redstone", {}) 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(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
@ -578,13 +578,13 @@ local function config_view(display)
tool_ctl.settings_confirm.hide() tool_ctl.settings_confirm.hide()
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="The following peripherals will be imported:"} 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=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} 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:"} 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=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} 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 --#endregion
--#region CONFIG CHANGE LOG --#region Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=49} 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} 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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
@ -635,7 +635,7 @@ local function config_view(display)
--#endregion --#endregion
--#region DEVICES --#region Peripherals
local peri_c_1 = Div{parent=peri_cfg,x=2,y=4,width=49} 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} 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)} 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() local function peri_revert()
tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals) tmp_cfg.Peripherals = deep_copy_peri(ini_cfg.Peripherals)
@ -659,7 +659,7 @@ local function config_view(display)
local function peri_apply() local function peri_apply()
settings.set("Peripherals", tmp_cfg.Peripherals) 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(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
peri_pane.set_value(5) 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."} 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=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} 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 --#endregion
--#region REDSTONE --#region Redstone
local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} 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} 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_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} 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() local function rs_revert()
tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone) tmp_cfg.Redstone = deep_copy_rs(ini_cfg.Redstone)
@ -973,7 +973,7 @@ local function config_view(display)
local function rs_apply() local function rs_apply()
settings.set("Redstone", tmp_cfg.Redstone) 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(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
rs_pane.set_value(4) 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."} 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) local function new_rs(port)
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
@ -1466,7 +1466,7 @@ local function reset_term()
end end
-- run the RTU gateway configurator -- 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) function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true tool_ctl.ask_config = ask_config == true

View File

@ -41,6 +41,8 @@ function rtu.load_config()
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_type_num(config.SpeakerVolume) cfv.assert_type_num(config.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(config.SVR_Channel)
cfv.assert_channel(config.RTU_Channel) cfv.assert_channel(config.RTU_Channel)
cfv.assert_type_num(config.ConnTimeout) cfv.assert_type_num(config.ConnTimeout)
@ -55,6 +57,7 @@ function rtu.load_config()
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath) cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug) cfv.assert_type_bool(config.LogDebug)

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_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_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE 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) -- try to reconfigure (user action)
local success, error = configure.configure(true) local success, error = configure.configure(true)
if success then 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 else
assert(success, "RTU configuration error: " .. error) println("configuration error: " .. error)
return
end end
end end

View File

@ -17,7 +17,7 @@ local max_distance = nil
local comms = {} local comms = {}
-- protocol/data version (protocol/data independent changes tracked by util.lua version) -- protocol/data version (protocol/data independent changes tracked by util.lua version)
comms.version = "2.4.3" comms.version = "2.4.5"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {

View File

@ -7,7 +7,7 @@ local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5") 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 pbkdf2 = require("lockbox.kdf.pbkdf2")
local hmac = require("lockbox.mac.hmac") local hmac = require("lockbox.mac.hmac")
local stream = require("lockbox.util.stream") local stream = require("lockbox.util.stream")
@ -31,12 +31,12 @@ function network.init_mac(passkey)
local key_deriv = pbkdf2() local key_deriv = pbkdf2()
-- setup 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.setSalt("pepper")
key_deriv.setIterations(32) key_deriv.setPassword(passkey)
key_deriv.setBlockLen(8)
key_deriv.setDKeyLen(16)
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256))
key_deriv.finish() key_deriv.finish()
c_eng.key = array.fromHex(key_deriv.asHex()) c_eng.key = array.fromHex(key_deriv.asHex())
@ -53,6 +53,11 @@ function network.init_mac(passkey)
return init_time return init_time
end end
-- de-initialize message authentication system
function network.deinit_mac()
c_eng.key, c_eng.hmac = nil, nil
end
-- generate HMAC of message -- generate HMAC of message
---@nodiscard ---@nodiscard
---@param message string initial value concatenated with ciphertext ---@param message string initial value concatenated with ciphertext

View File

@ -9,7 +9,7 @@ local util = require("scada-common.util")
local ppm = {} local ppm = {}
local ACCESS_FAULT = nil ---@type nil local ACCESS_FAULT = nil ---@type nil
local UNDEFINED_FIELD = "undefined field" local UNDEFINED_FIELD = "__PPM_UNDEF_FIELD__"
local VIRTUAL_DEVICE_TYPE = "ppm_vdev" local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
ppm.ACCESS_FAULT = ACCESS_FAULT ppm.ACCESS_FAULT = ACCESS_FAULT
@ -155,7 +155,7 @@ local function peri_init(iface)
self.fault_counts[key] = self.fault_counts[key] + 1 self.fault_counts[key] = self.fault_counts[key] + 1
return (function () return ACCESS_FAULT end) return (function () return UNDEFINED_FIELD end)
end end
} }
@ -300,6 +300,17 @@ function ppm.handle_unmount(iface)
return pm_type, pm_dev return pm_type, pm_dev
end 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 -- -- GENERAL ACCESSORS --
-- list all available peripherals -- list all available peripherals
@ -421,4 +432,15 @@ function ppm.get_monitor_list()
return list return list
end end
-- HELPER FUNCTIONS
-- get the block size of a monitor given its width and height <b>at a text scale of 0.5</b>
---@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 return ppm

View File

@ -22,7 +22,7 @@ local t_pack = table.pack
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.1.12" util.version = "1.1.18"
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50
@ -284,11 +284,13 @@ function util.cancel_timer(timer) os.cancelTimer(timer) end
--#region PARALLELIZATION --#region PARALLELIZATION
-- protected sleep call so we still are in charge of catching termination -- protected sleep call so we still are in charge of catching termination<br>
---@param t integer seconds -- returns the result of pcall
---@param t number seconds
---@return boolean success, any result, any ...
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
---@diagnostic disable-next-line: undefined-field ---@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<br> -- no-op to provide a brief pause (1 tick) to yield<br>
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
@ -348,6 +350,16 @@ function util.table_contains(t, element)
return false return false
end 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 --#endregion
--#region MEKANISM POWER --#region MEKANISM POWER

View File

@ -2,7 +2,7 @@ local util = require("scada-common.util")
local println = util.println local println = util.println
local BOOTLOADER_VERSION = "0.5" local BOOTLOADER_VERSION = "1.0"
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION) println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
println("BOOT> SCANNING FOR APPLICATIONS...") println("BOOT> SCANNING FOR APPLICATIONS...")

View File

@ -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}} 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 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=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} 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_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} 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=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="..."} 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} 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 } 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} 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_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_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=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} 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} 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=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} 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_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_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)} 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() local function back_from_settings()
if tool_ctl.viewing_config or tool_ctl.importing_legacy then 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() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end 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(settings_cfg, true)
load_settings(ini_cfg) 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=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)} 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} 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} 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 for _, change in ipairs(changes) do
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg} 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} 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} 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)) 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] == "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 = "" val = ""
for idx = 1, #cfg.CoolingConfig do for idx = 1, #cfg.CoolingConfig do
@ -982,7 +990,7 @@ local function config_view(display)
if val == "" then val = "no facility tanks" end 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] == "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 = "" val = ""
for idx = 1, #cfg.FacilityTankDefs do for idx = 1, #cfg.FacilityTankDefs do
@ -1033,7 +1041,7 @@ local function reset_term()
end end
-- run the supervisor configurator -- 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) function configurator.configure(ask_config)
tool_ctl.ask_config = ask_config == true tool_ctl.ask_config = ask_config == true

View File

@ -152,6 +152,8 @@ function facility.new(num_reactors, cooling_conf)
table.insert(self.test_tone_states, false) table.insert(self.test_tone_states, false)
end end
-- PRIVATE FUNCTIONS --
-- check if all auto-controlled units completed ramping -- check if all auto-controlled units completed ramping
---@nodiscard ---@nodiscard
local function _all_units_ramped() local function _all_units_ramped()
@ -228,7 +230,7 @@ function facility.new(num_reactors, cooling_conf)
---@class facility ---@class facility
local public = {} local public = {}
-- ADD/LINK DEVICES -- --#region Add/Link Devices
-- link a redstone RTU session -- link a redstone RTU session
---@param rs_unit unit_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 for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end
end end
-- UPDATE -- --#endregion
-- supervisor sessions reporting the list of active RTU sessions --#region Update
---@param rtu_sessions table session list of all connected RTUs
function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end
-- update (iterate) the facility management -- update (iterate) the facility management
function public.update() function public.update()
@ -323,7 +323,7 @@ function facility.new(num_reactors, cooling_conf)
-- Run Process Control -- -- Run Process Control --
------------------------- -------------------------
--#region Process Control --#region
local avg_charge = self.avg_charge.compute() local avg_charge = self.avg_charge.compute()
local avg_inflow = self.avg_inflow.compute() local avg_inflow = self.avg_inflow.compute()
@ -337,7 +337,7 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then if state_changed then
self.saturated = false 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 if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK self.start_fail = START_STATUS.OK
@ -375,6 +375,8 @@ function facility.new(num_reactors, cooling_conf)
end end
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 if blade_count == nil then
-- no units -- no units
log.warning("FAC: cannot start process control with 0 units assigned") 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.saturated = true
self.status_text = { "MONITORED MODE", "running reactors at limit" } 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 end
_allocate_burn_rate(self.max_burn_combined, true) _allocate_burn_rate(self.max_burn_combined, true)
@ -445,7 +447,7 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then if state_changed then
self.time_start = now self.time_start = now
self.status_text = { "BURN RATE MODE", "running" } 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 end
local unallocated = _allocate_burn_rate(self.burn_target, true) local unallocated = _allocate_burn_rate(self.burn_target, true)
@ -459,7 +461,7 @@ function facility.new(num_reactors, cooling_conf)
self.accumulator = 0 self.accumulator = 0
self.status_text = { "CHARGE MODE", "running control loop" } 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 elseif self.last_update ~= charge_update then
-- convert to kFE to make constants not microscopic -- convert to kFE to make constants not microscopic
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000 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 -- -- Evaluate Automatic SCRAM --
------------------------------ ------------------------------
--#region Automatic SCRAM --#region
local astatus = self.ascram_status 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) 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 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 end
-- check for critical unit alarms -- check for critical unit alarms
@ -725,6 +727,8 @@ function facility.new(num_reactors, cooling_conf)
-- Handle Redstone I/O -- -- Handle Redstone I/O --
------------------------- -------------------------
--#region
if #self.redstone > 0 then if #self.redstone > 0 then
-- handle facility SCRAM -- handle facility SCRAM
if self.io_ctl.digital_read(IO.F_SCRAM) then 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) self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
end end
--#endregion
---------------- ----------------
-- Unit Tasks -- -- Unit Tasks --
---------------- ----------------
--#region
local insufficent_po_rate = false local insufficent_po_rate = false
local need_emcool = false local need_emcool = false
@ -796,10 +804,14 @@ function facility.new(num_reactors, cooling_conf)
end end
end end
--#endregion
------------------------ ------------------------
-- Update Alarm Tones -- -- Update Alarm Tones --
------------------------ ------------------------
--#region
local allow_test = self.allow_testing and self.test_tone_set 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 } 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_set = false
self.test_tone_reset = true self.test_tone_reset = true
end end
--#endregion
end end
-- call the update function of all units in the facility<br> -- call the update function of all units in the facility<br>
@ -898,7 +912,9 @@ function facility.new(num_reactors, cooling_conf)
end end
end end
-- COMMANDS -- --#endregion
--#region Commands
-- SCRAM all reactor units -- SCRAM all reactor units
function public.scram_all() function public.scram_all()
@ -986,7 +1002,9 @@ function facility.new(num_reactors, cooling_conf)
} }
end end
-- SETTINGS -- --#endregion
--#region Settings
-- set the automatic control group of a unit -- set the automatic control group of a unit
---@param unit_id integer unit ID ---@param unit_id integer unit ID
@ -1027,7 +1045,9 @@ function facility.new(num_reactors, cooling_conf)
return self.pu_fallback return self.pu_fallback
end end
-- DIAGNOSTIC TESTING -- --#endregion
--#region Diagnostic Testing
-- attempt to set a test tone state -- attempt to set a test tone state
---@param id TONE|0 tone ID or 0 to disable all ---@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 return self.allow_testing, self.test_alarm_states
end end
-- READ STATES/PROPERTIES -- --#endregion
--#region Read States/Properties
-- get current alarm tone on/off states -- get current alarm tone on/off states
---@nodiscard ---@nodiscard
@ -1181,6 +1203,12 @@ function facility.new(num_reactors, cooling_conf)
return status return status
end 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 -- get the units in this facility
---@nodiscard ---@nodiscard
function public.get_units() return self.units end function public.get_units() return self.units end

View File

@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.2.5" local SUPERVISOR_VERSION = "v1.2.11"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -34,9 +34,13 @@ if not supervisor.load_config() then
-- try to reconfigure (user action) -- try to reconfigure (user action)
local success, error = configure.configure(true) local success, error = configure.configure(true)
if success then 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 else
assert(success, "supervisor configuration error: " .. error) println("configuration error: " .. error)
return
end end
end end

View File

@ -69,6 +69,7 @@ function supervisor.load_config()
cfv.assert_min(config.PKT_Timeout, 2) cfv.assert_min(config.PKT_Timeout, 2)
cfv.assert_type_num(config.TrustedRange) cfv.assert_type_num(config.TrustedRange)
cfv.assert_min(config.TrustedRange, 0)
if type(config.AuthKey) == "string" then if type(config.AuthKey) == "string" then
local len = string.len(config.AuthKey) local len = string.len(config.AuthKey)
@ -76,6 +77,7 @@ function supervisor.load_config()
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(config.LogMode)
cfv.assert_range(config.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath) cfv.assert_type_str(config.LogPath)
cfv.assert_type_bool(config.LogDebug) cfv.assert_type_bool(config.LogDebug)
@ -197,9 +199,8 @@ function supervisor.comms(_version, nic, fp_ok)
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) session.in_queue.push_packet(packet)
else else
-- unknown session, force a re-link -- any other packet should be session related, discard it
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") log.debug("discarding RPLC packet without a known session")
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame

View File

@ -77,7 +77,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
tanks = {}, tanks = {},
snas = {}, snas = {},
envd = {}, envd = {},
sna_prod_rate = 0,
-- redstone control -- redstone control
io_ctl = nil, ---@type rs_controller io_ctl = nil, ---@type rs_controller
valves = {}, ---@type unit_valves valves = {}, ---@type unit_valves
@ -256,7 +255,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
--#region time derivative utility functions --#region Time Derivative Utility Functions
-- compute a change with respect to time of the given value -- compute a change with respect to time of the given value
---@param key string value key ---@param key string value key
@ -331,7 +330,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
--#region redstone I/O --#region Redstone I/O
-- create a generic valve interface -- create a generic valve interface
---@nodiscard ---@nodiscard
@ -398,8 +397,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
---@class reactor_unit ---@class reactor_unit
local public = {} local public = {}
-- ADD/LINK DEVICES -- --#region Add/Link Devices
--#region
-- link the PLC -- link the PLC
---@param plc_session plc_session_struct ---@param plc_session plc_session_struct
@ -489,7 +487,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
-- UPDATE SESSION -- --#region Update Session
-- update (iterate) this unit -- update (iterate) this unit
function public.update() function public.update()
@ -557,13 +555,15 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
end end
-- AUTO CONTROL OPERATIONS -- --#endregion
--#region
--#region Auto Control Operations
-- engage automatic control -- engage automatic control
function public.auto_engage() function public.auto_engage()
self.auto_engaged = true self.auto_engaged = true
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": engaged auto control"))
self.plc_i.auto_lock(true) self.plc_i.auto_lock(true)
end end
end end
@ -572,6 +572,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
function public.auto_disengage() function public.auto_disengage()
self.auto_engaged = false self.auto_engaged = false
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control"))
self.plc_i.auto_lock(false) self.plc_i.auto_lock(false)
self.db.control.br100 = 0 self.db.control.br100 = 0
end end
@ -582,12 +583,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
---@nodiscard ---@nodiscard
---@return integer lim_br100 ---@return integer lim_br100
function public.auto_get_effective_limit() function public.auto_get_effective_limit()
if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then local ctrl = self.db.control
self.db.control.br100 = 0 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 return 0
else else return ctrl.lim_br100 end
return self.db.control.lim_br100
end
end end
-- set the automatic burn rate based on the last set burn rate in 100ths -- 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) function public.auto_commit_br100(ramp)
if self.auto_engaged then if self.auto_engaged then
if self.plc_i ~= nil 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) self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
if ramp then self.ramp_target_br100 = self.db.control.br100 end if ramp then self.ramp_target_br100 = self.db.control.br100 end
end end
end end
@ -643,8 +644,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
-- OPERATIONS -- --#region Operations
--#region
-- queue a command to disable the reactor -- queue a command to disable the reactor
function public.disable() function public.disable()
@ -724,8 +724,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#endregion --#endregion
-- READ STATES/PROPERTIES -- --#region Read States/Properties
--#region
-- check if an alarm of at least a certain priority level is tripped -- check if an alarm of at least a certain priority level is tripped
---@nodiscard ---@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 } status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks }
end end
-- basic SNA statistical information -- SNA statistical information
local total_peak = 0 local total_peak, total_avail, total_out = 0, 0, 0
for i = 1, #self.snas do for i = 1, #self.snas do
local db = self.snas[i].get_db() ---@type sna_session_db local db = self.snas[i].get_db() ---@type sna_session_db
total_peak = total_peak + db.state.peak_production 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 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) -- radiation monitors (environment detectors)
status.envds = {} status.envds = {}
@ -874,7 +875,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
return status return status
end end
-- get the current total [max] production rate is -- get the current total max production rate
---@nodiscard ---@nodiscard
---@return number total_avail_rate ---@return number total_avail_rate
function public.get_sna_rate() function public.get_sna_rate()

View File

@ -54,9 +54,7 @@ function logic.update_annunciator(self)
-- variables for boiler, or reactor if no boilers used -- variables for boiler, or reactor if no boilers used
local total_boil_rate = 0.0 local total_boil_rate = 0.0
------------- --#region Reactor
-- REACTOR --
-------------
annunc.AutoControl = self.auto_engaged annunc.AutoControl = self.auto_engaged
@ -143,9 +141,9 @@ function logic.update_annunciator(self)
self.plc_cache.ok = false self.plc_cache.ok = false
end end
--------------- --#endregion
-- MISC RTUs --
--------------- --#region Misc RTUs
local max_rad, any_faulted = 0, false local max_rad, any_faulted = 0, false
@ -170,9 +168,9 @@ function logic.update_annunciator(self)
end end
end end
------------- --#endregion
-- BOILERS --
------------- --#region Boilers
local boilers_ready = num_boilers == #self.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) boiler_water_dt_sum = _get_dt(DT_KEYS.ReactorCCool)
end end
--------------------------- --#endregion
-- COOLANT FEED MISMATCH --
--------------------------- --#region Coolant Feed Mismatch
-- check coolant feed mismatch if using boilers, otherwise calculate with reactor -- check coolant feed mismatch if using boilers, otherwise calculate with reactor
local cfmismatch = false local cfmismatch = false
@ -263,9 +261,9 @@ function logic.update_annunciator(self)
annunc.CoolantFeedMismatch = cfmismatch annunc.CoolantFeedMismatch = cfmismatch
-------------- --#endregion
-- TURBINES --
-------------- --#region Turbines
local turbines_ready = num_turbines == #self.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 annunc.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0
end end
--#endregion
-- update auto control ready state for this unit -- update auto control ready state for this unit
self.db.control.ready = plc_ready and boilers_ready and turbines_ready self.db.control.ready = plc_ready and boilers_ready and turbines_ready
end end

View File

@ -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
]]--