Compare commits

..

No commits in common. "main" and "v1.8.21-beta" have entirely different histories.

178 changed files with 8189 additions and 13917 deletions

View File

@ -1,57 +0,0 @@
# Contribution Guide
>[!NOTE]
Until the system is out of beta, contributions will be limited as I wrap up the specific release feature set.
This project is highly complex for a ComputerCraft Lua application. Contributions need to follow style guides and meet the code quality I've kept this project up to for years. Contributions must be tested appropriately with test results included.
I have extensively tested software components for stability required for safety, with tiers of software robustness.
1. **Critical: High-Impact** -
The Reactor-PLC is "uncrashable" and must remain so. I've extensively reviewed every line and behavior, so any code contributions must be at this high standard. Simple is stable, so the less code the better. Always check for parameter validity and extensively test any changes to critical thread functions.
2. **Important: Moderate-Impact** -
The Supervisor and RTU Gateway should rarely, if ever, crash. Certain places may not be held to as strict of a level as above, but should be written understanding all the possible inputs to and impacts of a section of code.
3. **Useful: Low-Impact** -
The Coordinator and Pocket are nice UI apps, and things can break. There's a lot of data going to and from them, so checking every single incoming value would have negative performance impacts and increase program size. If they break, the user can restart them. Don't introduce careless bugs, but making assumptions about the integrity of incoming data is acceptable.
## Valuable Contributions
Pull requests should not consist of purely whitespace changes, comment changes, or other trivial changes. They should target specific features, bug fixes, or functional improvements. I reserve the right to decline PRs that don't follow this in good faith.
## Project Management Guidelines
Any contributions should be linked to an open GitHub issue. These are used to track progress, discuss changes, etc. Surprise changes to this project might conflict with existing plans, so I prefer we coordinate changes ahead of time.
## Software Guidelines
These guidelines are subject to change. The general rule is make the code look like the rest of the code around it and elsewhere in the project.
### Style Guide
PRs will only be accepted if they match the style of this project and pass manual and automated code analysis. Listing out the whole style guide would take a while, so as stated above, please review code adjacent to your modifications.
1. **No Block Comments.**
These interfere with the minification used for the bundled installation files due to the complexity of parsing Lua block comments. The minification code is meant to be simple to have 0 risk of breaking anything, so I'm staying far away from those.
2. **Comment Your Code.**
This includes type hints as used elsewhere throughout the project. Your comments should be associated with parts of code that are more complex or unclear, or otherwise to split up sections of tasks. You'll see `--#region` used in various places.
- Type hints are intended to be utilized by the `sumneko.lua` vscode extension. You should use this while developing, as it provides extremely valuable functionality.
3. **Whitespace Usage.**
Whitespace should be used to separate function parameters and operators. The one exception is the unique styling of graphics elements, which you should compare against if modifying them.
- 4 spaces are used for all indentation.
- Try to align assignment operator lines as is done elsewhere (adding space before `=`).
- Use empty new lines to separate steps or distinct groups of operations.
- Generally add new lines for each step in loops and for statements. For some single-line ones, they may be compressed into a single line. This saves on space utilization, especially on deeply indented lines.
4. **Variables and Classes.**
- Variables, functions, and class-like tables follow the snake_case convention.
- Graphics objects and configuration settings follow PascalCase.
- Constants follow all-caps SNAKE_CASE and local ones should be declared at the top of files after `require` statements and external ones (like `local ALARM = types.ALARM`).
5. **No `goto`.**
These are generally frowned upon due to reducing code readability.
6. **Multiple `return`s.**
These are allowed to minimize code size, but if it is simple to avoid multiple, do so.
7. **Classes and Objects.**
Review the existing code for examples on how objects are implemented in this project. They do not use Lua's `:` operator and `self` functionality. A manual object-like table definition is used. Some global single-instance classes don't use a `new()` function, such as the [PPM](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/scada-common/ppm.lua). Multi-instance ones do, such as the Supervisor's [unit](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/supervisor/unit.lua) class.
### No AI
Your code should follow the style guide, be succinct, make sense, and you should be able to explain what it does. Random changes done in multiple places will be deemed suspicious along with poor comments or nonsensical code.
Use your contributions as programming practice or to hone your skills; don't automate away thinking.

View File

@ -2,6 +2,7 @@ import base64
import json
import os
import subprocess
import sys
path_prefix = "./_minified/"

View File

@ -28,9 +28,6 @@ def minify(path: str):
contents = f.read()
f.close()
# remove --[[@as type]] hints before anything, since it would detect as multiline comments
contents = re.sub(r' --+\[.+]]', '', contents)
if re.search(r'--+\[+', contents) != None:
# absolutely not dealing with lua multiline comments
# - there are more important things to do

367
ccmsi.lua
View File

@ -15,11 +15,11 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]--
local CCMSI_VERSION = "v1.21"
local CCMSI_VERSION = "v1.17"
local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://git.befatorinc.de/TheHomecraft/cc-mek-scada/raw/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
---@diagnostic disable-next-line: undefined-global
local _is_pkt_env = pocket -- luacheck: ignore pocket
@ -149,16 +149,16 @@ local function get_remote_manifest()
end
-- record the local installation manifest
local function write_install_manifest(manifest, deps)
local function write_install_manifest(manifest, dependencies)
local versions = {}
for key, value in pairs(manifest.versions) do
local is_dep = false
for _, dep in pairs(deps) do
if (key == "bootloader" and dep == "system") or key == dep then
is_dep = true;break
local is_dependency = false
for _, dependency in pairs(dependencies) do
if (key == "bootloader" and dependency == "system") or key == dependency then
is_dependency = true;break
end
end
if key == app or key == "comms" or is_dep then versions[key] = value end
if key == app or key == "comms" or is_dependency then versions[key] = value end
end
manifest.versions = versions
@ -169,7 +169,6 @@ local function write_install_manifest(manifest, deps)
end
-- try at most 3 times to download a file from the repository and write into w_path base directory
---@return 0|1|2|3 success 0: ok, 1: download fail, 2: file open fail, 3: out of space
local function http_get_file(file, w_path)
local dl, err
for i = 1, 3 do
@ -177,28 +176,17 @@ local function http_get_file(file, w_path)
if dl then
if i > 1 then green();println("success!");lgray() end
local f = fs.open(w_path..file, "w")
if not f then return 2 end
local ok, msg = pcall(function() f.write(dl.readAll()) end)
f.write(dl.readAll())
f.close()
if not ok then
if string.find(msg or "", "Out of space") ~= nil then
red();println("[out of space]");lgray()
return 3
else return 2 end
end
break
else
red();println("HTTP Error: "..err)
if i < 3 then
lgray();print("> retrying...")
---@diagnostic disable-next-line: undefined-field
os.sleep(i/3.0)
else
return 1
end
if i < 3 then lgray();print("> retrying...") end
---@diagnostic disable-next-line: undefined-field
os.sleep(i/3.0)
end
end
return 0
return dl ~= nil
end
-- recursively build a tree out of the file manifest
@ -321,38 +309,23 @@ if #opts == 0 or opts[1] == "help" then
end
return
else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
if mode == nil then
red();println("Unrecognized mode.");white()
return
end
local next_opt = 3
local apps = { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }
app = get_opt(opts[2], apps)
if app == nil then
for _, a in pairs(apps) do
if fs.exists(a) and fs.isDir(a) then
app = a
next_opt = 2
break
end
end
end
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" })
if app == nil and mode ~= "check" then
red();println("Unrecognized application.");white()
return
elseif mode == "check" then
next_opt = 2
elseif app == "installer" and mode ~= "update" then
red();println("Installer app only supports 'update' option.");white()
return
end
-- determine target
target = opts[next_opt]
if mode == "check" then target = opts[2] else target = opts[3] end
if (target ~= "main") and (target ~= "devel") then
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
target = "main"
@ -398,10 +371,8 @@ if mode == "check" then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end
elseif mode == "install" or mode == "update" then
local ok, r_manifest, l_manifest
local update_installer = app == "installer"
ok, r_manifest = get_remote_manifest()
local ok, manifest = get_remote_manifest()
if not ok then return end
local ver = {
@ -414,29 +385,29 @@ elseif mode == "install" or mode == "update" then
}
-- try to find local versions
ok, l_manifest = read_local_manifest()
if mode == "update" and not update_installer then
if not ok then
local local_ok, lmnf = read_local_manifest()
if not local_ok then
if mode == "update" then
red();println("Failed to load local installation information, cannot update.");white()
return
else
ver.boot.v_local = l_manifest.versions.bootloader
ver.app.v_local = l_manifest.versions[app]
ver.comms.v_local = l_manifest.versions.comms
ver.common.v_local = l_manifest.versions.common
ver.graphics.v_local = l_manifest.versions.graphics
ver.lockbox.v_local = l_manifest.versions.lockbox
end
elseif not update_installer then
ver.boot.v_local = lmnf.versions.bootloader
ver.app.v_local = lmnf.versions[app]
ver.comms.v_local = lmnf.versions.comms
ver.common.v_local = lmnf.versions.common
ver.graphics.v_local = lmnf.versions.graphics
ver.lockbox.v_local = lmnf.versions.lockbox
if l_manifest.versions[app] == nil then
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return
end
if lmnf.versions[app] == nil then
red();println("Another application is already installed, please uninstall it before installing a new application.");white()
return
end
end
if r_manifest.versions.installer ~= CCMSI_VERSION then
if manifest.versions.installer ~= CCMSI_VERSION then
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
if update_installer or ask_y_n("Would you like to update now", true) then
if update_installer or ask_y_n("Would you like to update now") then
lgray();println("GET ccmsi.lua")
local dl, err = http.get(repo_path.."ccmsi.lua")
@ -457,12 +428,12 @@ elseif mode == "install" or mode == "update" then
return
end
ver.boot.v_remote = r_manifest.versions.bootloader
ver.app.v_remote = r_manifest.versions[app]
ver.comms.v_remote = r_manifest.versions.comms
ver.common.v_remote = r_manifest.versions.common
ver.graphics.v_remote = r_manifest.versions.graphics
ver.lockbox.v_remote = r_manifest.versions.lockbox
ver.boot.v_remote = manifest.versions.bootloader
ver.app.v_remote = manifest.versions[app]
ver.comms.v_remote = manifest.versions.comms
ver.common.v_remote = manifest.versions.common
ver.graphics.v_remote = manifest.versions.graphics
ver.lockbox.v_remote = manifest.versions.lockbox
green()
if mode == "install" then print("Installing ") else print("Updating ") end
@ -478,33 +449,36 @@ elseif mode == "install" or mode == "update" then
ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- start install/update
--------------------------
-- START INSTALL/UPDATE --
--------------------------
local space_req = r_manifest.sizes.manifest
local space_avail = fs.getFreeSpace("/")
local space_required = manifest.sizes.manifest
local space_available = fs.getFreeSpace("/")
local file_list = r_manifest.files
local size_list = r_manifest.sizes
local deps = r_manifest.depends[app]
local single_file_mode = false
local file_list = manifest.files
local size_list = manifest.sizes
local dependencies = manifest.depends[app]
table.insert(deps, app)
table.insert(dependencies, app)
-- helper function to check if a dependency is unchanged
local function unchanged(dep)
if dep == "system" then return not ver.boot.changed
elseif dep == "graphics" then return not ver.graphics.changed
elseif dep == "lockbox" then return not ver.lockbox.changed
elseif dep == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dep == app then return not ver.app.changed
local function unchanged(dependency)
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed
elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed
else return true end
end
local any_change = false
for _, dep in pairs(deps) do
local size = size_list[dep]
space_req = space_req + size
any_change = any_change or not unchanged(dep)
for _, dependency in pairs(dependencies) do
local size = size_list[dependency]
space_required = space_required + size
any_change = any_change or not unchanged(dependency)
end
if mode == "update" and not any_change then
@ -515,143 +489,38 @@ elseif mode == "install" or mode == "update" then
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
local single_file_mode = space_avail < space_req
-- check space constraints
if space_available < space_required then
single_file_mode = true
yellow();println("NOTICE: Insufficient space available for a full cached download!");white()
lgray();println("Files can instead be downloaded one by one. If you are replacing a current install this may corrupt your install ONLY if it fails (such as a sudden network issue). If that occurs, you can still try again.")
if mode == "update" then println("If installation still fails, delete this device's log file and/or any unrelated files you have on this computer then try again.") end
white();
if not ask_y_n("Do you wish to continue", false) then
println("Operation cancelled.")
return
end
end
local success = true
-- delete a file if the capitalization changes so that things work on Windows
---@param path string
local function mitigate_case(path)
local dir, file = fs.getDir(path), fs.getName(path)
if not fs.isDir(dir) then return end
for _, p in ipairs(fs.list(dir)) do
if string.lower(p) == string.lower(file) then
if p ~= file then fs.delete(path) end
return
end
end
end
---@param dl_stat 1|2|3 download status
---@param file string file name
---@param attempt integer recursive attempt #
---@param sf_install function installer function for recursion
local function handle_dl_fail(dl_stat, file, attempt, sf_install)
red()
if dl_stat == 1 then
println("failed to download "..file)
elseif dl_stat > 1 then
if dl_stat == 2 then println("filesystem error with "..file) else println("no space for "..file) end
if attempt == 1 then
orange();println("re-attempting operation...");white()
sf_install(2)
elseif attempt == 2 then
yellow()
if dl_stat == 2 then println("There was an error writing to a file.") else println("Insufficient space available.") end
lgray()
if dl_stat == 2 then
println("This may be due to insufficent space available or file permission issues. The installer can now attempt to delete files not used by the SCADA system.")
else
println("The installer can now attempt to delete files not used by the SCADA system.")
end
white()
if not ask_y_n("Continue", false) then
success = false
return
end
clean(r_manifest)
sf_install(3)
elseif attempt == 3 then
yellow()
if dl_stat == 2 then println("There again was an error writing to a file.") else println("Insufficient space available.") end
lgray()
if dl_stat == 2 then
println("This may be due to insufficent space available or file permission issues. Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
else
println("Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
end
white()
success = false
end
end
end
-- single file update routine: go through all files and replace one by one
---@param attempt integer recursive attempt #
local function sf_install(attempt)
---@diagnostic disable-next-line: undefined-field
if attempt > 1 then os.sleep(2.0) end
local abort_attempt = false
success = true
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
else
pkg_message("installing package", dep)
lgray()
-- beginning on the second try, delete the directory before starting
if attempt >= 2 then
if dep == "system" then
elseif dep == "common" then
if fs.exists("/scada-common") then
fs.delete("/scada-common")
println("deleted /scada-common")
end
else
if fs.exists("/"..dep) then
fs.delete("/"..dep)
println("deleted /"..dep)
end
end
end
local files = file_list[dep]
for _, file in pairs(files) do
println("GET "..file)
mitigate_case(file)
local dl_stat = http_get_file(file, "/")
if dl_stat ~= 0 then
abort_attempt = true
---@diagnostic disable-next-line: param-type-mismatch
handle_dl_fail(dl_stat, file, attempt, sf_install)
break
end
end
end
if abort_attempt or not success then break end
end
end
-- handle update/install
if single_file_mode then sf_install(1)
else
if not single_file_mode then
if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
-- download all dependencies
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping download of unchanged package", dep)
for _, dependency in pairs(dependencies) do
if mode == "update" and unchanged(dependency) then
pkg_message("skipping download of unchanged package", dependency)
else
pkg_message("downloading package", dep)
pkg_message("downloading package", dependency)
lgray()
local files = file_list[dep]
local files = file_list[dependency]
for _, file in pairs(files) do
println("GET "..file)
local dl_stat = http_get_file(file, install_dir.."/")
success = dl_stat == 0
if dl_stat == 1 then
if not http_get_file(file, install_dir.."/") then
red();println("failed to download "..file)
break
elseif dl_stat == 2 then
red();println("filesystem error with "..file)
break
elseif dl_stat == 3 then
-- this shouldn't occur in this mode
red();println("no space for "..file)
success = false
break
end
end
@ -661,14 +530,14 @@ elseif mode == "install" or mode == "update" then
-- copy in downloaded files (installation)
if success then
for _, dep in pairs(deps) do
if mode == "update" and unchanged(dep) then
pkg_message("skipping install of unchanged package", dep)
for _, dependency in pairs(dependencies) do
if mode == "update" and unchanged(dependency) then
pkg_message("skipping install of unchanged package", dependency)
else
pkg_message("installing package", dep)
pkg_message("installing package", dependency)
lgray()
local files = file_list[dep]
local files = file_list[dependency]
for _, file in pairs(files) do
local temp_file = install_dir.."/"..file
if fs.exists(file) then fs.delete(file) end
@ -679,27 +548,57 @@ elseif mode == "install" or mode == "update" then
end
fs.delete(install_dir)
end
if success then
write_install_manifest(r_manifest, deps)
green()
if mode == "install" then
println("Installation completed successfully.")
else println("Update completed successfully.") end
white();println("Ready to clean up unused files, press any key to continue...")
any_key();clean(r_manifest)
white();println("Done.")
if success then
write_install_manifest(manifest, dependencies)
green()
if mode == "install" then
println("Installation completed successfully.")
else println("Update completed successfully.") end
white();println("Ready to clean up unused files, press any key to continue...")
any_key();clean(manifest)
white();println("Done.")
else
if mode == "install" then
red();println("Installation failed.")
else orange();println("Update failed, existing files unmodified.") end
end
else
red()
if single_file_mode then
-- go through all files and replace one by one
for _, dependency in pairs(dependencies) do
if mode == "update" and unchanged(dependency) then
pkg_message("skipping install of unchanged package", dependency)
else
pkg_message("installing package", dependency)
lgray()
local files = file_list[dependency]
for _, file in pairs(files) do
println("GET "..file)
if not http_get_file(file, "/") then
red();println("failed to download "..file)
success = false
break
end
end
end
if not success then break end
end
if success then
write_install_manifest(manifest, dependencies)
green()
if mode == "install" then
println("Installation completed successfully.")
else println("Update completed successfully.") end
white();println("Ready to clean up unused files, press any key to continue...")
any_key();clean(manifest)
white();println("Done.")
else
red()
if mode == "install" then
println("Installation failed, files may have been skipped.")
else println("Update failed, files may have been skipped.") end
else
if mode == "install" then
println("Installation failed.")
else orange();println("Update failed, existing files unmodified.") end
end
end
elseif mode == "uninstall" then
@ -723,14 +622,14 @@ elseif mode == "uninstall" then
clean(manifest)
local file_list = manifest.files
local deps = manifest.depends[app]
local dependencies = manifest.depends[app]
table.insert(deps, app)
table.insert(dependencies, app)
-- delete all installed files
lgray()
for _, dep in pairs(deps) do
local files = file_list[dep]
for _, dependency in pairs(dependencies) do
local files = file_list[dependency]
for _, file in pairs(files) do
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
end

View File

@ -1,12 +1,11 @@
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
for _, app in ipairs({ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" }) do
if fs.exists(app .. "/configure.lua") then
local _, _, launch = require(app .. ".configure").configure()
if launch then shell.execute("/startup") end
return
end
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
else
print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT")
end
print("CONFIGURE> NO CONFIGURATOR FOUND")
print("CONFIGURE> EXIT")

View File

@ -1,318 +0,0 @@
local comms = require("scada-common.comms")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local NumberField = require("graphics.elements.form.NumberField")
local tri = util.trinary
local cpair = core.cpair
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
show_sv_cfg = nil, ---@type function
sv_conn_button = nil, ---@type PushButton
sv_conn_status = nil, ---@type TextBox
sv_conn_detail = nil, ---@type TextBox
sv_next = nil, ---@type PushButton
sv_skip = nil, ---@type PushButton
tool_ctl = nil, ---@type _crd_cfg_tool_ctl
tmp_cfg = nil ---@type crd_config
}
-- check if a value is an integer within a range (inclusive)
---@param x any
---@param min integer
---@param max integer
local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end
-- send a management packet to the supervisor
---@param msg_type MGMT_TYPE
---@param msg table
local function send_sv(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.tmp_cfg.SVR_Channel, self.tmp_cfg.CRD_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
---@param packet mgmt_frame
local function handle_packet(packet)
local error_msg = nil
if packet.scada_frame.local_channel() ~= self.tmp_cfg.CRD_Channel then
error_msg = "Error: unknown receive channel."
elseif packet.scada_frame.remote_channel() == self.tmp_cfg.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if packet.type == MGMT_TYPE.ESTABLISH then
if packet.length == 2 then
local est_ack = packet.data[1]
local config = packet.data[2]
if est_ack == ESTABLISH_ACK.ALLOW then
if type(config) == "table" and #config == 2 then
local count_ok = is_int_min_max(config[1], 1, 4)
local cool_ok = type(config[2]) == "table" and type(config[2].r_cool) == "table" and #config[2].r_cool == config[1]
if count_ok and cool_ok then
self.tmp_cfg.UnitCount = config[1]
self.tool_ctl.sv_cool_conf = {}
for i = 1, self.tmp_cfg.UnitCount do
local num_b = config[2].r_cool[i].BoilerCount
local num_t = config[2].r_cool[i].TurbineCount
self.tool_ctl.sv_cool_conf[i] = { num_b, num_t }
cool_ok = cool_ok and is_int_min_max(num_b, 0, 2) and is_int_min_max(num_t, 1, 3)
end
end
if not count_ok then
error_msg = "Error: supervisor unit count out of range."
elseif not cool_ok then
error_msg = "Error: supervisor cooling configuration malformed."
self.tool_ctl.sv_cool_conf = nil
end
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
else
error_msg = "Error: invalid cooling configuration supervisor."
end
else
error_msg = "Error: invalid allow reply length from supervisor."
end
elseif packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.DENY then
error_msg = "Error: supervisor connection denied."
elseif est_ack == ESTABLISH_ACK.COLLISION then
error_msg = "Error: a coordinator is already/still connected. Please try again."
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
error_msg = "Error: coordinator comms version does not match supervisor comms version."
else
error_msg = "Error: invalid reply from supervisor."
end
else
error_msg = "Error: invalid reply length from supervisor."
end
else
error_msg = "Error: didn't get an establish reply from supervisor."
end
end
self.net_listen = false
if error_msg then
self.sv_conn_status.set_value("")
self.sv_conn_detail.set_value(error_msg)
self.sv_conn_button.enable()
else
self.sv_conn_status.set_value("Connected!")
self.sv_conn_detail.set_value("Data received successfully, press 'Next' to continue.")
self.sv_skip.hide()
self.sv_next.show()
end
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.sv_conn_button.enable()
self.sv_conn_status.set_value("Timed out.")
self.sv_conn_detail.set_value("Supervisor did not reply. Ensure startup app is running on the supervisor.")
end
-- attempt a connection to the supervisor to get cooling info
local function sv_connect()
self.sv_conn_button.disable()
self.sv_conn_detail.set_value("")
local modem = ppm.get_wireless_modem()
if modem == nil then
self.sv_conn_status.set_value("Please connect an ender/wireless modem.")
else
self.sv_conn_status.set_value("Modem found, connecting...")
if self.nic == nil then self.nic = network.nic(modem) end
self.nic.closeAll()
self.nic.open(self.tmp_cfg.CRD_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.CRD })
tcd.dispatch_unique(8, handle_timeout)
end
end
local facility = {}
-- create the facility configuration view
---@param tool_ctl _crd_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param fac_cfg Div
---@param style { [string]: cpair }
---@return MultiPane fac_pane
function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
self.tmp_cfg = tmp_cfg
self.tool_ctl = tool_ctl
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Facility
local fac_c_1 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_2 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_3 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3}}
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=fac_c_1,x=1,y=1,height=4,text="This tool can attempt to connect to your supervisor computer. This would load facility information in order to get the unit count and aid monitor setup."}
TextBox{parent=fac_c_1,x=1,y=6,height=2,text="The supervisor startup app must be running and fully configured on your supervisor computer."}
self.sv_conn_status = TextBox{parent=fac_c_1,x=11,y=9,text=""}
self.sv_conn_detail = TextBox{parent=fac_c_1,x=1,y=11,height=2,text=""}
self.sv_conn_button = PushButton{parent=fac_c_1,x=1,y=9,text="Connect",min_width=9,callback=function()sv_connect()end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
local function sv_skip()
tcd.abort(handle_timeout)
tool_ctl.sv_cool_conf = nil
self.net_listen = false
fac_pane.set_value(2)
end
local function sv_next()
self.show_sv_cfg()
tool_ctl.update_mon_reqs()
fac_pane.set_value(3)
end
PushButton{parent=fac_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.sv_skip = PushButton{parent=fac_c_1,x=44,y=14,text="Skip \x1a",callback=sv_skip,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
self.sv_next = PushButton{parent=fac_c_1,x=44,y=14,text="Next \x1a",callback=sv_next,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,hidden=true}
TextBox{parent=fac_c_2,x=1,y=1,height=3,text="Please enter the number of reactors you have, also referred to as reactor units or 'units' for short. A maximum of 4 is currently supported."}
tool_ctl.num_units = NumberField{parent=fac_c_2,x=1,y=5,width=5,max_chars=2,default=ini_cfg.UnitCount,min=1,max=4,fg_bg=bw_fg_bg}
TextBox{parent=fac_c_2,x=7,y=5,text="reactors"}
TextBox{parent=fac_c_2,x=1,y=7,height=3,text="This will decide how many monitors you need. If this does not match the supervisor's number of reactor units, the coordinator will not connect.",fg_bg=cpair(colors.yellow,colors._INHERIT)}
TextBox{parent=fac_c_2,x=1,y=10,height=3,text="Since you skipped supervisor sync, the main monitor minimum height can't be determined precisely. It is marked with * on the next page.",fg_bg=g_lg_fg_bg}
local nu_error = TextBox{parent=fac_c_2,x=8,y=14,width=35,text="Please set the number of reactors.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_num_units()
local count = tonumber(tool_ctl.num_units.get_value())
if count ~= nil and count > 0 and count < 5 then
nu_error.hide(true)
tmp_cfg.UnitCount = count
tool_ctl.update_mon_reqs()
main_pane.set_value(4)
else nu_error.show() end
end
PushButton{parent=fac_c_2,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_2,x=44,y=14,text="Next \x1a",callback=submit_num_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=fac_c_3,x=1,y=1,height=2,text="The following facility configuration was fetched from your supervisor computer."}
local fac_config_list = ListBox{parent=fac_c_3,x=1,y=4,height=9,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
PushButton{parent=fac_c_3,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_3,x=44,y=14,text="Next \x1a",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Tool and Helper Functions
tool_ctl.is_int_min_max = is_int_min_max
-- reset the connection display for a new attempt
function tool_ctl.init_sv_connect_ui()
self.sv_next.hide()
self.sv_skip.disable()
self.sv_skip.show()
self.sv_conn_button.enable()
self.sv_conn_status.set_value("")
self.sv_conn_detail.set_value("")
-- the user needs to wait a few seconds, encouraging the to connect
tcd.dispatch_unique(2, function () self.sv_skip.enable() end)
end
-- show the facility's unit count and cooling configuration data
function self.show_sv_cfg()
local conf = tool_ctl.sv_cool_conf
fac_config_list.remove_all()
local str = util.sprintf("Facility has %d reactor unit%s:", #conf, tri(#conf==1,"","s"))
TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
for i = 1, #conf do
local num_b, num_t = conf[i][1], conf[i][2]
str = util.sprintf("\x07 Unit %d has %d boiler%s and %d turbine%s", i, num_b, tri(num_b == 1, "", "s"), num_t, tri(num_t == 1, "", "s"))
TextBox{parent=fac_config_list,text=str,fg_bg=cpair(colors.gray,colors.white)}
end
end
--#endregion
return fac_pane
end
-- handle incoming modem messages
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
function facility.receive_sv(side, sender, reply_to, message, distance)
if self.nic ~= nil and self.net_listen then
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
tcd.abort(handle_timeout)
handle_packet(mgmt_pkt.get())
end
end
end
end
return facility

View File

@ -1,455 +0,0 @@
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local cpair = core.cpair
local self = {
apply_mon = nil, ---@type PushButton
edit_monitor = nil, ---@type function
mon_iface = "",
mon_expect = {} ---@type integer[]
}
local hmi = {}
-- create the HMI (human machine interface) configuration view
---@param tool_ctl _crd_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param divs Div[]
---@param style { [string]: cpair }
---@return MultiPane mon_pane
function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
local _, ini_cfg, tmp_cfg, _, _ = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
local mon_cfg, spkr_cfg, crd_cfg = divs[1], divs[2], divs[3]
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Monitors
local mon_c_1 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_c_2 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_c_3 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_c_4 = Div{parent=mon_cfg,x=2,y=4,width=49}
local mon_pane = MultiPane{parent=mon_cfg,x=1,y=4,panes={mon_c_1,mon_c_2,mon_c_3,mon_c_4}}
TextBox{parent=mon_cfg,x=1,y=2,text=" Monitor Configuration",fg_bg=cpair(colors.black,colors.blue)}
TextBox{parent=mon_c_1,x=1,y=1,height=5,text="Your configuration requires the following monitors. The main and flow monitors' heights are dependent on your unit count and cooling setup. If you manually entered the unit count, a * will be shown on potentially inaccurate calculations."}
local mon_reqs = ListBox{parent=mon_c_1,x=1,y=7,height=6,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function next_from_reqs()
-- unassign unit monitors above the unit count
for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end
tool_ctl.gen_mon_list()
mon_pane.set_value(2)
end
PushButton{parent=mon_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=mon_c_1,x=8,y=14,text="Legacy Options",min_width=16,callback=function()mon_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=mon_c_1,x=44,y=14,text="Next \x1a",callback=next_from_reqs,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=mon_c_2,x=1,y=1,height=5,text="Please configure your monitors below. You can go back to the prior page without losing progress to double check what you need. All of those monitors must be assigned before you can proceed."}
local mon_list = ListBox{parent=mon_c_2,x=1,y=6,height=7,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local assign_err = TextBox{parent=mon_c_2,x=8,y=14,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_monitors()
if tmp_cfg.MainDisplay == nil then
assign_err.set_value("Please assign the main monitor.")
elseif tmp_cfg.FlowDisplay == nil and not tmp_cfg.DisableFlowView then
assign_err.set_value("Please assign the flow monitor.")
elseif util.table_len(tmp_cfg.UnitDisplays) ~= tmp_cfg.UnitCount then
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == nil then
assign_err.set_value("Please assign the unit " .. i .. " monitor.")
break
end
end
else
assign_err.hide(true)
main_pane.set_value(5)
return
end
assign_err.show()
end
PushButton{parent=mon_c_2,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=mon_c_2,x=44,y=14,text="Next \x1a",callback=submit_monitors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local mon_desc = TextBox{parent=mon_c_3,x=1,y=1,height=4,text=""}
local mon_unit_l, mon_unit = nil, nil ---@type TextBox, NumberField
local mon_warn = TextBox{parent=mon_c_3,x=1,y=11,height=2,text="",fg_bg=cpair(colors.red,colors.lightGray)}
---@param val integer assignment type
local function on_assign_mon(val)
if val == 2 and tmp_cfg.DisableFlowView then
self.apply_mon.disable()
mon_warn.set_value("You disabled having a flow view monitor. It can't be set unless you go back and enable it.")
mon_warn.show()
elseif not util.table_contains(self.mon_expect, val) then
self.apply_mon.disable()
mon_warn.set_value("That assignment doesn't fit monitor dimensions. You'll need to resize the monitor for it to work.")
mon_warn.show()
else
self.apply_mon.enable()
mon_warn.hide(true)
end
if val == 3 then
mon_unit_l.show()
mon_unit.show()
else
mon_unit_l.hide(true)
mon_unit.hide(true)
end
local value = mon_unit.get_value()
mon_unit.set_max(tmp_cfg.UnitCount)
if value == "0" or value == nil then mon_unit.set_value(0) end
end
TextBox{parent=mon_c_3,x=1,y=6,width=10,text="Assignment"}
local mon_assign = RadioButton{parent=mon_c_3,x=1,y=7,default=1,options={"Main Monitor","Flow Monitor","Unit Monitor"},callback=on_assign_mon,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.blue}
mon_unit_l = TextBox{parent=mon_c_3,x=18,y=6,width=7,text="Unit ID"}
mon_unit = NumberField{parent=mon_c_3,x=18,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local mon_u_err = TextBox{parent=mon_c_3,x=8,y=14,width=35,text="Please provide a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
-- purge all assignments for a given monitor
---@param iface string
local function purge_assignments(iface)
if tmp_cfg.MainDisplay == iface then
tmp_cfg.MainDisplay = nil
elseif tmp_cfg.FlowDisplay == iface then
tmp_cfg.FlowDisplay = nil
else
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then tmp_cfg.UnitDisplays[i] = nil end
end
end
end
local function apply_monitor()
local iface = self.mon_iface
local type = mon_assign.get_value()
local u_id = tonumber(mon_unit.get_value())
if type == 1 then
purge_assignments(iface)
tmp_cfg.MainDisplay = iface
elseif type == 2 then
purge_assignments(iface)
tmp_cfg.FlowDisplay = iface
elseif u_id and u_id > 0 then
purge_assignments(iface)
tmp_cfg.UnitDisplays[u_id] = iface
else
mon_u_err.show()
return
end
tool_ctl.gen_mon_list()
mon_u_err.hide(true)
mon_pane.set_value(2)
end
PushButton{parent=mon_c_3,x=1,y=14,text="\x1b Back",callback=function()mon_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.apply_mon = PushButton{parent=mon_c_3,x=43,y=14,min_width=7,text="Apply",callback=apply_monitor,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
TextBox{parent=mon_c_4,x=1,y=1,height=3,text="For legacy compatibility with facilities built without space for a flow monitor, you can disable the flow monitor requirement here."}
TextBox{parent=mon_c_4,x=1,y=5,height=3,text="Please be aware that THIS OPTION WILL BE REMOVED ON RELEASE. Disabling it will only be available for the remainder of the beta."}
tool_ctl.dis_flow_view = Checkbox{parent=mon_c_4,x=1,y=9,default=ini_cfg.DisableFlowView,label="Disable Flow View Monitor",box_fg_bg=cpair(colors.blue,colors.black)}
local function back_from_legacy()
tmp_cfg.DisableFlowView = tool_ctl.dis_flow_view.get_value()
tool_ctl.update_mon_reqs()
mon_pane.set_value(1)
end
PushButton{parent=mon_c_4,x=44,y=14,min_width=6,text="Done",callback=back_from_legacy,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Speaker
local spkr_c = Div{parent=spkr_cfg,x=2,y=4,width=49}
TextBox{parent=spkr_cfg,x=1,y=2,text=" Speaker Configuration",fg_bg=cpair(colors.black,colors.cyan)}
TextBox{parent=spkr_c,x=1,y=1,height=2,text="The coordinator uses a speaker to play alarm sounds."}
TextBox{parent=spkr_c,x=1,y=4,height=3,text="You can change the speaker audio volume from the default. The range is 0.0 to 3.0, where 1.0 is standard volume."}
tool_ctl.s_vol = NumberField{parent=spkr_c,x=1,y=8,width=9,max_chars=7,allow_decimal=true,default=ini_cfg.SpeakerVolume,min=0,max=3,fg_bg=bw_fg_bg}
TextBox{parent=spkr_c,x=1,y=10,height=3,text="Note: alarm sine waves are at half scale so that multiple will be required to reach full scale.",fg_bg=g_lg_fg_bg}
local s_vol_err = TextBox{parent=spkr_c,x=8,y=14,width=35,text="Please set a volume.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_vol()
local vol = tonumber(tool_ctl.s_vol.get_value())
if vol ~= nil then
s_vol_err.hide(true)
tmp_cfg.SpeakerVolume = vol
main_pane.set_value(6)
else s_vol_err.show() end
end
PushButton{parent=spkr_c,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=spkr_c,x=44,y=14,text="Next \x1a",callback=submit_vol,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Coordinator UI
local crd_c_1 = Div{parent=crd_cfg,x=2,y=4,width=49}
TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."}
TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=8,text="Energy Scale"}
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts()
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1
tmp_cfg.GreenPuPellet = tool_ctl.pellet_color.get_value() == 1
tmp_cfg.TempScale = tool_ctl.temp_scale.get_value()
tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value()
main_pane.set_value(7)
end
PushButton{parent=crd_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=crd_c_1,x=44,y=14,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Tool and Helper Functions
-- update list of monitor requirements
function tool_ctl.update_mon_reqs()
local plural = tmp_cfg.UnitCount > 1
if tool_ctl.sv_cool_conf ~= nil then
local cnf = tool_ctl.sv_cool_conf
local row1_tall = cnf[1][1] > 1 or cnf[1][2] > 2 or (cnf[2] and (cnf[2][1] > 1 or cnf[2][2] > 2))
local row1_short = (cnf[1][1] == 0 and cnf[1][2] == 1) and (cnf[2] == nil or (cnf[2][1] == 0 and cnf[2][2] == 1))
local row2_tall = (cnf[3] and (cnf[3][1] > 1 or cnf[3][2] > 2)) or (cnf[4] and (cnf[4][1] > 1 or cnf[4][2] > 2))
local row2_short = (cnf[3] == nil or (cnf[3][1] == 0 and cnf[3][2] == 1)) and (cnf[4] == nil or (cnf[4][1] == 0 and cnf[4][2] == 1))
if tmp_cfg.UnitCount <= 2 then
tool_ctl.main_mon_h = util.trinary(row1_tall, 5, 4)
else
-- is only one tall and the other short, or are both tall? -> 5 or 6; are neither tall? -> 5
if row1_tall or row2_tall then
tool_ctl.main_mon_h = util.trinary((row1_short and row2_tall) or (row1_tall and row2_short), 5, 6)
else tool_ctl.main_mon_h = 5 end
end
else
tool_ctl.main_mon_h = util.trinary(tmp_cfg.UnitCount <= 2, 4, 5)
end
tool_ctl.flow_mon_h = 2 + tmp_cfg.UnitCount
local asterisk = util.trinary(tool_ctl.sv_cool_conf == nil, "*", "")
local m_at_least = util.trinary(tool_ctl.main_mon_h < 6, "at least ", "")
local f_at_least = util.trinary(tool_ctl.flow_mon_h < 6, "at least ", "")
mon_reqs.remove_all()
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a "..tmp_cfg.UnitCount.." Unit View Monitor"..util.trinary(plural,"s","")}
TextBox{parent=mon_reqs,x=1,y=1,text=" "..util.trinary(plural,"each ","").."must be 4 blocks wide by 4 tall",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Main View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..m_at_least..tool_ctl.main_mon_h..asterisk.." tall",fg_bg=cpair(colors.gray,colors.white)}
if not tmp_cfg.DisableFlowView then
TextBox{parent=mon_reqs,x=1,y=1,text="\x1a 1 Flow View Monitor"}
TextBox{parent=mon_reqs,x=1,y=1,text=" must be 8 blocks wide by "..f_at_least..tool_ctl.flow_mon_h.." tall",fg_bg=cpair(colors.gray,colors.white)}
end
end
-- set/edit a monitor's assignment
---@param iface string
---@param device ppm_entry
function self.edit_monitor(iface, device)
self.mon_iface = iface
local dev = device.dev
local w, h = ppm.monitor_block_size(dev.getSize())
local msg = "This size doesn't match a required screen. Please go back and resize it, or configure below at the risk of it not working."
self.mon_expect = {}
mon_assign.set_value(1)
mon_unit.set_value(0)
if w == 4 and h == 4 then
msg = "This could work as a unit display. Please configure below."
self.mon_expect = { 3 }
mon_assign.set_value(3)
elseif w == 8 then
if h >= tool_ctl.main_mon_h and h >= tool_ctl.flow_mon_h then
msg = "This could work as either your main monitor or flow monitor. Please configure below."
self.mon_expect = { 1, 2 }
if tmp_cfg.MainDisplay then mon_assign.set_value(2) end
elseif h >= tool_ctl.main_mon_h then
msg = "This could work as your main monitor. Please configure below."
self.mon_expect = { 1 }
elseif h >= tool_ctl.flow_mon_h then
msg = "This could work as your flow monitor. Please configure below."
self.mon_expect = { 2 }
mon_assign.set_value(2)
end
end
-- override if a config exists
if tmp_cfg.MainDisplay == iface then
mon_assign.set_value(1)
elseif tmp_cfg.FlowDisplay == iface then
mon_assign.set_value(2)
else
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then
mon_assign.set_value(3)
mon_unit.set_value(i)
break
end
end
end
on_assign_mon(mon_assign.get_value())
mon_desc.set_value(util.c("You have selected '", iface, "', which has a block size of ", w, " wide by ", h, " tall. ", msg))
mon_pane.set_value(3)
end
-- generate the list of available monitors
function tool_ctl.gen_mon_list()
mon_list.remove_all()
local missing = { main = tmp_cfg.MainDisplay ~= nil, flow = tmp_cfg.FlowDisplay ~= nil, unit = {} }
for i = 1, tmp_cfg.UnitCount do missing.unit[i] = tmp_cfg.UnitDisplays[i] ~= nil end
-- list connected monitors
local monitors = ppm.get_monitor_list()
for iface, device in pairs(monitors) do
local dev = device.dev ---@type Monitor
dev.setTextScale(0.5)
dev.setTextColor(colors.white)
dev.setBackgroundColor(colors.black)
dev.clear()
dev.setCursorPos(1, 1)
dev.setTextColor(colors.magenta)
dev.write("This is monitor")
dev.setCursorPos(1, 2)
dev.setTextColor(colors.white)
dev.write(iface)
local assignment = "Unused"
if tmp_cfg.MainDisplay == iface then
assignment = "Main"
missing.main = false
elseif tmp_cfg.FlowDisplay == iface then
assignment = "Flow"
missing.flow = false
else
for i = 1, tmp_cfg.UnitCount do
if tmp_cfg.UnitDisplays[i] == iface then
missing.unit[i] = false
assignment = "Unit " .. i
break
end
end
end
local line = Div{parent=mon_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=6,text=assignment,fg_bg=cpair(util.trinary(assignment=="Unused",colors.red,colors.blue),colors.white)}
TextBox{parent=line,x=8,y=1,text=iface}
local w, h = ppm.monitor_block_size(dev.getSize())
local function unset_mon()
purge_assignments(iface)
tool_ctl.gen_mon_list()
end
TextBox{parent=line,x=33,y=1,width=4,text=w.."x"..h,fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()self.edit_monitor(iface,device)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local unset = PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
if assignment == "Unused" then unset.disable() end
end
local dc_list = {} -- disconnected monitor list
if missing.main then table.insert(dc_list, { "Main", tmp_cfg.MainDisplay }) end
if missing.flow then table.insert(dc_list, { "Flow", tmp_cfg.FlowDisplay }) end
for i = 1, tmp_cfg.UnitCount do
if missing.unit[i] then table.insert(dc_list, { "Unit " .. i, tmp_cfg.UnitDisplays[i] }) end
end
-- add monitors that are assigned but not connected
for i = 1, #dc_list do
local line = Div{parent=mon_list,x=1,y=1,height=1}
TextBox{parent=line,x=1,y=1,width=6,text=dc_list[i][1],fg_bg=cpair(colors.blue,colors.white)}
TextBox{parent=line,x=8,y=1,text="disconnected",fg_bg=cpair(colors.red,colors.white)}
local function unset_mon()
purge_assignments(dc_list[i][2])
tool_ctl.gen_mon_list()
end
TextBox{parent=line,x=33,y=1,width=4,text="?x?",fg_bg=cpair(colors.black,colors.white)}
PushButton{parent=line,x=37,y=1,min_width=5,height=1,text="SET",callback=function()end,dis_fg_bg=cpair(colors.black,colors.gray)}.disable()
PushButton{parent=line,x=42,y=1,min_width=7,height=1,text="UNSET",callback=unset_mon,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.black,colors.gray)}
end
end
--#endregion
return mon_pane
end
return hmi

View File

@ -1,580 +0,0 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local network = require("scada-common.network")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
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.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local TextField = require("graphics.elements.form.TextField")
local IndLight = require("graphics.elements.indicators.IndicatorLight")
local tri = util.trinary
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
local system = {}
-- create the system configuration view
---@param tool_ctl _crd_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ crd_config, crd_config, crd_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param divs Div[]
---@param ext [ MultiPane, MultiPane, function, function ]
---@param style { [string]: cpair }
function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
local net_cfg, log_cfg, clr_cfg, summary = divs[1], divs[2], divs[3], divs[4]
local fac_pane, mon_pane, preset_monitor_fields, exit = ext[1], ext[2], ext[3], ext[4]
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Network
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=49}
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
TextBox{parent=net_cfg,x=1,y=2,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=1,text="Please set the network channels below."}
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 3 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=8,width=18,text="Supervisor Channel"}
local svr_chan = NumberField{parent=net_c_1,x=21,y=8,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=8,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=10,width=19,text="Coordinator Channel"}
local crd_chan = NumberField{parent=net_c_1,x=21,y=10,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=10,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_1,x=1,y=12,width=14,text="Pocket Channel"}
local pkt_chan = NumberField{parent=net_c_1,x=21,y=12,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
TextBox{parent=net_c_1,x=29,y=12,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
local chan_err = TextBox{parent=net_c_1,x=8,y=14,width=35,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_channels()
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
net_pane.set_value(2)
chan_err.hide(true)
else chan_err.show() end
end
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_2,x=1,y=1,text="Please set the connection timeouts below."}
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally should not need to modify these. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection. The default for all is 5 seconds.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_2,x=1,y=8,width=19,text="Supervisor Timeout"}
local svr_timeout = NumberField{parent=net_c_2,x=20,y=8,width=7,default=ini_cfg.SVR_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=1,y=10,width=14,text="Pocket Timeout"}
local api_timeout = NumberField{parent=net_c_2,x=20,y=10,width=7,default=ini_cfg.API_Timeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
TextBox{parent=net_c_2,x=28,y=8,height=4,width=7,text="seconds\n\nseconds",fg_bg=g_lg_fg_bg}
local ct_err = TextBox{parent=net_c_2,x=8,y=14,width=35,text="Please set all connection timeouts.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_timeouts()
local svr_cto, api_cto = tonumber(svr_timeout.get_value()), tonumber(api_timeout.get_value())
if svr_cto ~= nil and api_cto ~= nil then
tmp_cfg.SVR_Timeout, tmp_cfg.API_Timeout = svr_cto, api_cto
net_pane.set_value(3)
ct_err.hide(true)
else ct_err.show() end
end
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_3,x=1,y=1,text="Please set the trusted range below."}
TextBox{parent=net_c_3,x=1,y=3,height=3,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_3,x=1,y=7,height=2,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
local range = NumberField{parent=net_c_3,x=1,y=10,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
local tr_err = TextBox{parent=net_c_3,x=8,y=14,width=35,text="Please set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_tr()
local range_val = tonumber(range.get_value())
if range_val ~= nil then
tmp_cfg.TrustedRange = range_val
comms.set_trusted_range(range_val)
net_pane.set_value(4)
tr_err.hide(true)
else tr_err.show() end
end
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=net_c_4,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
TextBox{parent=net_c_4,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra computation (can slow things down).",fg_bg=g_lg_fg_bg}
TextBox{parent=net_c_4,x=1,y=11,text="Facility Auth Key"}
local key, _ = TextField{parent=net_c_4,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) key.censor(tri(enable, "*", nil)) end
local hide_key = Checkbox{parent=net_c_4,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
hide_key.set_value(true)
censor_key(true)
local key_err = TextBox{parent=net_c_4,x=8,y=14,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_auth()
local v = key.get_value()
if string.len(v) == 0 or string.len(v) >= 8 then
tmp_cfg.AuthKey = key.get_value()
key_err.hide(true)
-- init mac for supervisor connection
if string.len(v) >= 8 then network.init_mac(tmp_cfg.AuthKey) else network.deinit_mac() end
-- prep supervisor connection screen
tool_ctl.init_sv_connect_ui()
main_pane.set_value(3)
else key_err.show() end
end
PushButton{parent=net_c_4,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=net_c_4,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Logging
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
TextBox{parent=log_cfg,x=1,y=2,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Please configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,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,text="Log File Path"}
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
local en_dbg = Checkbox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
local path_err = TextBox{parent=log_c_1,x=8,y=14,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
local function submit_log()
if path.get_value() ~= "" then
path_err.hide(true)
tmp_cfg.LogMode = mode.get_value() - 1
tmp_cfg.LogPath = path.get_value()
tmp_cfg.LogDebug = en_dbg.get_value()
tool_ctl.color_apply.hide(true)
tool_ctl.color_next.show()
main_pane.set_value(8)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Color Options
local clr_c_1 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_2 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_3 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_c_4 = Div{parent=clr_cfg,x=2,y=4,width=49}
local clr_pane = MultiPane{parent=clr_cfg,x=1,y=4,panes={clr_c_1,clr_c_2,clr_c_3,clr_c_4}}
TextBox{parent=clr_cfg,x=1,y=2,text=" Color Configuration",fg_bg=cpair(colors.black,colors.magenta)}
TextBox{parent=clr_c_1,x=1,y=1,height=2,text="Here you can select the color themes for the different UI displays."}
TextBox{parent=clr_c_1,x=1,y=4,height=2,text="Click 'Accessibility' below to access colorblind assistive options.",fg_bg=g_lg_fg_bg}
TextBox{parent=clr_c_1,x=1,y=7,text="Main UI Theme"}
local main_theme = RadioButton{parent=clr_c_1,x=1,y=8,default=ini_cfg.MainTheme,options=themes.UI_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_1,x=18,y=7,text="Front Panel Theme"}
local fp_theme = RadioButton{parent=clr_c_1,x=18,y=8,default=ini_cfg.FrontPanelTheme,options=themes.FP_THEME_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=1,y=1,height=6,text="This system uses color heavily to distinguish ok and not, with some indicators using many colors. By selecting a mode below, indicators will change as shown. For non-standard modes, indicators with more than two colors will usually be split up."}
TextBox{parent=clr_c_2,x=21,y=7,text="Preview"}
local _ = IndLight{parent=clr_c_2,x=21,y=8,label="Good",colors=cpair(colors.black,colors.green)}
_ = IndLight{parent=clr_c_2,x=21,y=9,label="Warning",colors=cpair(colors.black,colors.yellow)}
_ = IndLight{parent=clr_c_2,x=21,y=10,label="Bad",colors=cpair(colors.black,colors.red)}
local b_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.black,colors.black),hidden=true}
local g_off = IndLight{parent=clr_c_2,x=21,y=11,label="Off",colors=cpair(colors.gray,colors.gray),hidden=true}
local function recolor(value)
local c = themes.smooth_stone.color_modes[value]
if value == themes.COLOR_MODE.STANDARD or value == themes.COLOR_MODE.BLUE_IND then
b_off.hide()
g_off.show()
else
g_off.hide()
b_off.show()
end
if #c == 0 then
for i = 1, #style.colors do term.setPaletteColor(style.colors[i].c, style.colors[i].hex) end
else
term.setPaletteColor(colors.green, c[1].hex)
term.setPaletteColor(colors.yellow, c[2].hex)
term.setPaletteColor(colors.red, c[3].hex)
end
end
TextBox{parent=clr_c_2,x=1,y=7,width=10,text="Color Mode"}
local c_mode = RadioButton{parent=clr_c_2,x=1,y=8,default=ini_cfg.ColorMode,options=themes.COLOR_MODE_NAMES,callback=recolor,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.magenta}
TextBox{parent=clr_c_2,x=21,y=13,height=2,width=18,text="Note: exact color varies by theme.",fg_bg=g_lg_fg_bg}
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function back_from_colors()
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 7))
tool_ctl.jumped_to_color = false
recolor(1)
end
local function show_access()
clr_pane.set_value(2)
recolor(c_mode.get_value())
end
local function submit_colors()
tmp_cfg.MainTheme = main_theme.get_value()
tmp_cfg.FrontPanelTheme = fp_theme.get_value()
tmp_cfg.ColorMode = c_mode.get_value()
if tool_ctl.jumped_to_color then
settings.set("MainTheme", tmp_cfg.MainTheme)
settings.set("FrontPanelTheme", tmp_cfg.FrontPanelTheme)
settings.set("ColorMode", tmp_cfg.ColorMode)
if settings.save("/coordinator.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
clr_pane.set_value(3)
else
clr_pane.set_value(4)
end
else
tool_ctl.gen_summary(tmp_cfg)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(9)
end
end
PushButton{parent=clr_c_1,x=1,y=14,text="\x1b Back",callback=back_from_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=clr_c_1,x=8,y=14,min_width=15,text="Accessibility",callback=show_access,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.color_next = PushButton{parent=clr_c_1,x=44,y=14,text="Next \x1a",callback=submit_colors,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.color_apply = PushButton{parent=clr_c_1,x=43,y=14,min_width=7,text="Apply",callback=submit_colors,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
tool_ctl.color_apply.hide(true)
local function c_go_home()
main_pane.set_value(1)
clr_pane.set_value(1)
end
TextBox{parent=clr_c_3,x=1,y=1,text="Settings saved!"}
PushButton{parent=clr_c_3,x=1,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=clr_c_3,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=clr_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=clr_c_4,x=1,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=clr_c_4,x=44,y=14,min_width=6,text="Home",callback=c_go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Summary and Saving
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_3 = Div{parent=summary,x=2,y=4,width=49}
local sum_c_4 = Div{parent=summary,x=2,y=4,width=49}
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
TextBox{parent=summary,x=1,y=2,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function back_from_summary()
if tool_ctl.viewing_config or self.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(8)
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 _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/coordinator.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(crd_chan, ini_cfg.CRD_Channel)
try_set(pkt_chan, ini_cfg.PKT_Channel)
try_set(svr_timeout, ini_cfg.SVR_Timeout)
try_set(api_timeout, ini_cfg.API_Timeout)
try_set(range, ini_cfg.TrustedRange)
try_set(key, ini_cfg.AuthKey)
try_set(tool_ctl.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView)
try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume)
try_set(tool_ctl.pellet_color, ini_cfg.GreenPuPellet)
try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2))
try_set(tool_ctl.temp_scale, ini_cfg.TempScale)
try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale)
try_set(mode, ini_cfg.LogMode)
try_set(path, ini_cfg.LogPath)
try_set(en_dbg, ini_cfg.LogDebug)
try_set(main_theme, ini_cfg.MainTheme)
try_set(fp_theme, ini_cfg.FrontPanelTheme)
try_set(c_mode, ini_cfg.ColorMode)
preset_monitor_fields()
tool_ctl.gen_mon_list()
tool_ctl.view_cfg.enable()
tool_ctl.color_cfg.enable()
if self.importing_legacy then
self.importing_legacy = false
sum_pane.set_value(3)
else
sum_pane.set_value(2)
end
else
sum_pane.set_value(4)
end
end
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
TextBox{parent=sum_c_2,x=1,y=1,text="Settings saved!"}
local function go_home()
main_pane.set_value(1)
net_pane.set_value(1)
fac_pane.set_value(1)
mon_pane.set_value(1)
clr_pane.set_value(1)
sum_pane.set_value(1)
end
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua and coord.settings files will now be deleted, then the configurator will exit."}
local function delete_legacy()
fs.delete("/coordinator/config.lua")
fs.delete("/coord.settings")
exit()
end
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
--#endregion
--#region Tool Functions
-- load a legacy config file
function tool_ctl.load_legacy()
local config = require("coordinator.config")
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
tmp_cfg.SVR_Timeout = config.SV_TIMEOUT
tmp_cfg.API_Timeout = config.API_TIMEOUT
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
tmp_cfg.AuthKey = config.AUTH_KEY or ""
tmp_cfg.UnitCount = config.NUM_UNITS
tmp_cfg.DisableFlowView = config.DISABLE_FLOW_VIEW
tmp_cfg.SpeakerVolume = config.SOUNDER_VOLUME
tmp_cfg.Time24Hour = config.TIME_24_HOUR
tmp_cfg.LogMode = config.LOG_MODE
tmp_cfg.LogPath = config.LOG_PATH
tmp_cfg.LogDebug = config.LOG_DEBUG or false
settings.load("/coord.settings")
tmp_cfg.MainDisplay = settings.get("PRIMARY_DISPLAY")
tmp_cfg.FlowDisplay = settings.get("FLOW_DISPLAY")
tmp_cfg.UnitDisplays = settings.get("UNIT_DISPLAYS", {})
-- if there are extra monitor entries, delete them now
-- not doing so will cause the app to fail to start
if tool_ctl.is_int_min_max(tmp_cfg.UnitCount, 1, 4) then
for i = tmp_cfg.UnitCount + 1, 4 do tmp_cfg.UnitDisplays[i] = nil end
end
if settings.get("ControlStates") == nil then
local ctrl_states = {
process = settings.get("PROCESS"),
waste_modes = settings.get("WASTE_MODES"),
priority_groups = settings.get("PRIORITY_GROUPS"),
}
settings.set("ControlStates", ctrl_states)
end
settings.unset("PRIMARY_DISPLAY")
settings.unset("FLOW_DISPLAY")
settings.unset("UNIT_DISPLAYS")
settings.unset("PROCESS")
settings.unset("WASTE_MODES")
settings.unset("PRIORITY_GROUPS")
tool_ctl.gen_summary(tmp_cfg)
sum_pane.set_value(1)
main_pane.set_value(9)
self.importing_legacy = true
end
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.auth_key_value)
end
-- generate the summary list
---@param cfg crd_config
function tool_ctl.gen_summary(cfg)
setting_list.remove_all()
local alternate = false
local inner_width = setting_list.get_width() - 1
self.show_key_btn.enable()
self.auth_key_value = cfg.AuthKey or "" -- to show auth key
for i = 1, #fields do
local f = fields[i]
local height = 1
local label_w = string.len(f[2])
local val_max_w = (inner_width - label_w) + 1
local raw = cfg[f[1]]
local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
elseif f[1] == "MainTheme" then
val = util.strval(themes.ui_theme_name(raw))
elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw))
elseif f[1] == "ColorMode" then
val = util.strval(themes.color_mode_name(raw))
elseif f[1] == "UnitDisplays" and type(cfg.UnitDisplays) == "table" then
val = ""
for idx = 1, #cfg.UnitDisplays do
val = val .. util.trinary(idx == 1, "", "\n") .. util.sprintf(" \x07 Unit %d - %s", idx, cfg.UnitDisplays[idx])
end
end
if val == "nil" then val = "<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) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end
if (f[1] == "UnitDisplays") and (height == 1) and (val ~= "<not set>") then height = 2 end
local line = Div{parent=setting_list,height=height,fg_bg=c}
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
end
end
--#endregion
end
return system

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,6 @@ local LINK_TIMEOUT = 60.0
local coordinator = {}
---@type crd_config
---@diagnostic disable-next-line: missing-fields
local config = {}
coordinator.config = config
@ -38,7 +37,6 @@ function coordinator.load_config()
config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour")
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
@ -68,7 +66,6 @@ function coordinator.load_config()
cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
@ -114,12 +111,12 @@ function coordinator.load_config()
---@class monitors_struct
local monitors = {
main = nil, ---@type Monitor|nil
main = nil, ---@type table|nil
main_name = "",
flow = nil, ---@type Monitor|nil
flow = nil, ---@type table|nil
flow_name = "",
unit_displays = {}, ---@type Monitor[]
unit_name_map = {} ---@type string[]
unit_displays = {},
unit_name_map = {}
}
local mon_cfv = util.new_validator()
@ -382,18 +379,6 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end
-- send the resume ready state to the supervisor
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
mode, burn_target, charge_target, gen_target, limits
})
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
@ -402,14 +387,10 @@ function coordinator.comms(version, nic, sv_watchdog)
end
-- send the auto process control configuration with a start command
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
---@param auto_cfg sys_auto_config configuration
function public.send_auto_start(auto_cfg)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, mode, burn_target, charge_target, gen_target, limits
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
})
end
@ -597,7 +578,7 @@ function coordinator.comms(version, nic, sv_watchdog)
if cmd == FAC_COMMAND.SCRAM_ALL then
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.STOP then
process.fac_ack(cmd, ack)
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMAND.START then
if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) })
@ -642,7 +623,7 @@ function coordinator.comms(version, nic, sv_watchdog)
local unit_id = packet.data[2]
local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id]
local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then

View File

@ -20,13 +20,6 @@ local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
local RCT_STATE = types.REACTOR_STATE
local BLR_STATE = types.BOILER_STATE
local TRB_STATE = types.TURBINE_STATE
local TNK_STATE = types.TANK_STATE
local MTX_STATE = types.IMATRIX_STATE
local SPS_STATE = types.SPS_STATE
-- nominal RTT is ping (0ms to 10ms usually) + 500ms for CRD main loop tick
local WARN_RTT = 1000 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 1500 -- 3.33x as long as expected w/ 0 ping
@ -36,6 +29,15 @@ local iocontrol = {}
---@class ioctl
local io = {}
-- luacheck: no unused args
-- placeholder acknowledge function for type hinting
---@param success boolean
---@diagnostic disable-next-line: unused-local
local function __generic_ack(success) end
-- luacheck: unused args
-- initialize front panel PSIL
---@param firmware_v string coordinator version
---@param comms_v string comms version
@ -88,13 +90,9 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
tank_conns = conf.cooling.fac_tank_conns,
tank_fluid_types = conf.cooling.tank_fluid_types,
all_sys_ok = false,
rtu_count = 0,
status_lines = { "", "" },
auto_ready = false,
auto_active = false,
auto_ramping = false,
@ -103,7 +101,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_fault = false,
matrix_dc = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
@ -114,27 +112,28 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
auto_sps_disabled = false,
waste_stats = { 0, 0, 0, 0, 0, 0 }, -- waste in, pu, po, po pellets, am, spent waste
radiation = types.new_zero_radiation_reading(),
save_cfg_ack = nil, ---@type fun(success: boolean)
save_cfg_ack = __generic_ack,
start_ack = __generic_ack,
stop_ack = __generic_ack,
---@type { [TONE]: boolean }
alarm_tones = { false, false, false, false, false, false, false, false },
ps = psil.create(),
induction_ps_tbl = {}, ---@type psil[]
induction_data_tbl = {}, ---@type imatrix_session_db[]
induction_ps_tbl = {},
induction_data_tbl = {},
sps_ps_tbl = {}, ---@type psil[]
sps_data_tbl = {}, ---@type sps_session_db[]
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
tank_ps_tbl = {},
tank_data_tbl = {},
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
env_d_ps = psil.create(),
env_d_data = {}
}
-- create induction and SPS tables (currently only 1 of each is supported)
@ -152,7 +151,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
end
-- create unit data structures
io.units = {} ---@type ioctl_unit[]
io.units = {}
for i = 1, conf.num_units do
local function ack(alarm) process.ack_alarm(i, alarm) end
local function reset(alarm) process.reset_alarm(i, alarm) end
@ -161,17 +160,12 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
local entry = {
unit_id = i,
connected = false,
rtu_hw = { boilers = {}, turbines = {} },
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
aux_coolant = conf.cooling.aux_coolant[i],
status_lines = { "", "" },
auto_ready = false,
auto_degraded = false,
control_state = false,
burn_rate_cmd = 0.0,
@ -183,7 +177,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
waste_stats = { 0, 0, 0 }, -- plutonium, polonium, po pellets
last_rate_change_ms = 0,
turbine_flow_stable = false,
@ -215,7 +208,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
t_trip = { ack = function () ack(12) end, reset = function () reset(12) end }
},
---@type { [ALARM]: ALARM_STATE }
---@type alarms
alarms = {
ALARM_STATE.INACTIVE, -- containment breach
ALARM_STATE.INACTIVE, -- containment radiation
@ -231,22 +224,19 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
ALARM_STATE.INACTIVE -- turbine trip
},
---@diagnostic disable-next-line: missing-fields
annunciator = {}, ---@type annunciator
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
reactor_data = types.new_reactor_db(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {}, ---@type psil[]
boiler_data_tbl = {}, ---@type boilerv_session_db[]
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {}, ---@type psil[]
turbine_data_tbl = {}, ---@type turbinev_session_db[]
turbine_ps_tbl = {},
turbine_data_tbl = {},
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
tank_ps_tbl = {},
tank_data_tbl = {}
}
-- on other facility modes, overwrite unit TANK option with facility tank defs
@ -258,12 +248,14 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
end
-- create tank tables
@ -365,8 +357,8 @@ end
-- record and publish multiblock RTU build data
---@param id integer
---@param entry table
---@param data_tbl (imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db)[]
---@param ps_tbl psil[]
---@param data_tbl table
---@param ps_tbl table
---@param create boolean? true to create an entry if non exists, false to fail on missing
---@return boolean ok true if data saved, false if invalid ID
local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create)
@ -374,12 +366,11 @@ local function _record_multiblock_build(id, entry, data_tbl, ps_tbl, create)
if exists or create then
if not exists then
ps_tbl[id] = psil.create()
---@diagnostic disable-next-line: missing-fields
data_tbl[id] = {}
end
data_tbl[id].formed = entry[1]
data_tbl[id].build = entry[2]
data_tbl[id].formed = entry[1] ---@type boolean
data_tbl[id].build = entry[2] ---@type table
ps_tbl[id].publish("formed", entry[1])
@ -453,7 +444,7 @@ function iocontrol.record_unit_builds(builds)
else
-- reactor build
if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor
unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.unit_ps.publish(key, val)
end
@ -500,59 +491,16 @@ end
--#region Statuses
-- generate the text string for the induction matrix charge/discharge ETA
---@param eta_ms number eta in milliseconds
local function gen_eta_text(eta_ms)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
return str
end
-- record and publish multiblock status data
---@param entry any
---@param data imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db
---@param ps psil
---@return boolean is_faulted
local function _record_multiblock_status(entry, data, ps)
local is_faulted = entry[1]
data.formed = entry[2]
data.state = entry[3]
data.tanks = entry[4]
local is_faulted = entry[1] ---@type boolean
data.formed = entry[2] ---@type boolean
data.state = entry[3] ---@type table
data.tanks = entry[4] ---@type table
ps.publish("formed", data.formed)
ps.publish("faulted", is_faulted)
@ -595,14 +543,14 @@ function iocontrol.update_facility_status(status)
fac.auto_saturated = ctl_status[5]
fac.auto_scram = ctl_status[6]
fac.ascram_status.matrix_fault = ctl_status[7]
fac.ascram_status.matrix_dc = ctl_status[7]
fac.ascram_status.matrix_fill = ctl_status[8]
fac.ascram_status.crit_alarm = ctl_status[9]
fac.ascram_status.radiation = ctl_status[10]
fac.ascram_status.gen_fault = ctl_status[11]
fac.status_lines[1] = ctl_status[12]
fac.status_lines[2] = ctl_status[13]
fac.status_line_1 = ctl_status[12]
fac.status_line_2 = ctl_status[13]
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
fac.ps.publish("auto_ready", fac.auto_ready)
@ -610,13 +558,13 @@ function iocontrol.update_facility_status(status)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_saturated", fac.auto_saturated)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_dc", fac.ascram_status.matrix_dc)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
fac.ps.publish("status_line_1", fac.status_lines[1])
fac.ps.publish("status_line_2", fac.status_lines[2])
fac.ps.publish("status_line_1", fac.status_line_1)
fac.ps.publish("status_line_2", fac.status_line_2)
local group_map = ctl_status[14]
@ -652,8 +600,8 @@ function iocontrol.update_facility_status(status)
-- power statistics
if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then
local data = fac.induction_data_tbl[1]
local ps = fac.induction_ps_tbl[1]
local data = fac.induction_data_tbl[1] ---@type imatrix_session_db
local ps = fac.induction_ps_tbl[1] ---@type psil
local chg = tonumber(rtu_statuses.power[1])
local in_f = tonumber(rtu_statuses.power[2])
@ -664,7 +612,6 @@ function iocontrol.update_facility_status(status)
ps.publish("avg_inflow", in_f)
ps.publish("avg_outflow", out_f)
ps.publish("eta_ms", eta)
ps.publish("eta_string", gen_eta_text(eta or 0))
ps.publish("is_charging", in_f > out_f)
ps.publish("is_discharging", out_f > in_f)
@ -680,37 +627,33 @@ function iocontrol.update_facility_status(status)
-- induction matricies statuses
if type(rtu_statuses.induction) == "table" then
local matrix_status = MTX_STATE.OFFLINE
for id = 1, #fac.induction_ps_tbl do
if rtu_statuses.induction[id] == nil then
-- disconnected
fac.induction_ps_tbl[id].publish("computed_status", matrix_status)
fac.induction_ps_tbl[id].publish("computed_status", 1)
end
end
for id, matrix in pairs(rtu_statuses.induction) do
if type(fac.induction_data_tbl[id]) == "table" then
local data = fac.induction_data_tbl[id]
local ps = fac.induction_ps_tbl[id]
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
local ps = fac.induction_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(matrix, data, ps)
if rtu_faulted then
matrix_status = MTX_STATE.FAULT
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.energy_fill >= 0.99 then
matrix_status = MTX_STATE.HIGH_CHARGE
ps.publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then
matrix_status = MTX_STATE.LOW_CHARGE
ps.publish("computed_status", 5) -- empty
else
matrix_status = MTX_STATE.ONLINE
ps.publish("computed_status", 4) -- on-line
end
else
matrix_status = MTX_STATE.UNFORMED
ps.publish("computed_status", 2) -- not formed
end
ps.publish("computed_status", matrix_status)
else
log.debug(util.c(log_header, "invalid induction matrix id ", id))
end
@ -722,30 +665,31 @@ function iocontrol.update_facility_status(status)
-- SPS statuses
if type(rtu_statuses.sps) == "table" then
local sps_status = SPS_STATE.OFFLINE
for id = 1, #fac.sps_ps_tbl do
if rtu_statuses.sps[id] == nil then
-- disconnected
fac.sps_ps_tbl[id].publish("computed_status", sps_status)
fac.sps_ps_tbl[id].publish("computed_status", 1)
end
end
for id, sps in pairs(rtu_statuses.sps) do
if type(fac.sps_data_tbl[id]) == "table" then
local data = fac.sps_data_tbl[id]
local ps = fac.sps_ps_tbl[id]
local data = fac.sps_data_tbl[id] ---@type sps_session_db
local ps = fac.sps_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(sps, data, ps)
if rtu_faulted then
sps_status = SPS_STATE.FAULT
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
-- active / idle
sps_status = util.trinary(data.state.process_rate > 0, SPS_STATE.ACTIVE, SPS_STATE.IDLE)
else sps_status = SPS_STATE.UNFORMED end
ps.publish("computed_status", sps_status)
if data.state.process_rate > 0 then
ps.publish("computed_status", 5) -- active
else
ps.publish("computed_status", 4) -- idle
end
else
ps.publish("computed_status", 2) -- not formed
end
io.facility.ps.publish("am_rate", data.state.process_rate * 1000)
else
@ -759,35 +703,33 @@ function iocontrol.update_facility_status(status)
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
local tank_status = TNK_STATE.OFFLINE
for id = 1, #fac.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
-- disconnected
fac.tank_ps_tbl[id].publish("computed_status", tank_status)
fac.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(fac.tank_data_tbl[id]) == "table" then
local data = fac.tank_data_tbl[id]
local ps = fac.tank_ps_tbl[id]
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = fac.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
tank_status = TNK_STATE.FAULT
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
tank_status = TNK_STATE.HIGH_FILL
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
tank_status = TNK_STATE.LOW_FILL
ps.publish("computed_status", 5) -- low
else
tank_status = TNK_STATE.ONLINE
ps.publish("computed_status", 4) -- on-line
end
else tank_status = TNK_STATE.UNFORMED end
ps.publish("computed_status", tank_status)
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
end
@ -801,9 +743,7 @@ function iocontrol.update_facility_status(status)
if type(rtu_statuses.envds) == "table" then
local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
fac.rad_monitors = {}
for id, envd in pairs(rtu_statuses.envds) do
for _, envd in pairs(rtu_statuses.envds) do
local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
@ -815,10 +755,6 @@ function iocontrol.update_facility_status(status)
max_rad = rad_raw
max_reading = radiation
end
if not rtu_faulted then
fac.rad_monitors[id] = { radiation = radiation, raw = rad_raw }
end
end
if any_conn then
@ -876,7 +812,7 @@ function iocontrol.update_unit_statuses(statuses)
for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
local unit = io.units[i]
local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i]
local burn_rate = 0.0
@ -893,11 +829,9 @@ function iocontrol.update_unit_statuses(statuses)
log.debug(log_header .. "reactor status not a table")
end
local computed_status = RCT_STATE.OFFLINE
if #reactor_status == 0 then
unit.connected = false
unit.unit_ps.publish("computed_status", computed_status)
unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
@ -914,45 +848,50 @@ function iocontrol.update_unit_statuses(statuses)
log.debug(log_header .. "reactor general status length mismatch")
end
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
-- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate = unit.reactor_data.mek_status.act_burn_rate
burn_rate_sum = burn_rate_sum + burn_rate
end
if unit.reactor_data.mek_status.status then
unit.unit_ps.publish("computed_status", 5) -- running
else
if unit.reactor_data.no_reactor then
unit.unit_ps.publish("computed_status", 3) -- faulted
elseif not unit.reactor_data.formed then
unit.unit_ps.publish("computed_status", 2) -- multiblock not formed
elseif unit.reactor_data.rps_status.force_dis then
unit.unit_ps.publish("computed_status", 7) -- reactor force disabled
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.unit_ps.publish("computed_status", 6) -- SCRAM
else
unit.unit_ps.publish("computed_status", 4) -- disabled
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
unit.reactor_data.rps_status = rps_status
for key, val in pairs(rps_status) do
unit.unit_ps.publish(key, val)
end
if next(mek_status) then
unit.reactor_data.mek_status = mek_status
for key, val in pairs(mek_status) do
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
burn_rate = unit.reactor_data.mek_status.act_burn_rate
burn_rate_sum = burn_rate_sum + burn_rate
if unit.reactor_data.mek_status.status then
computed_status = RCT_STATE.ACTIVE
else
if unit.reactor_data.no_reactor then
computed_status = RCT_STATE.FAULT
elseif not unit.reactor_data.formed then
computed_status = RCT_STATE.UNFORMED
elseif unit.reactor_data.rps_status.force_dis then
computed_status = RCT_STATE.FORCE_DISABLED
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
computed_status = RCT_STATE.SCRAMMED
else
computed_status = RCT_STATE.DISABLED
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
unit.connected = true
unit.unit_ps.publish("computed_status", computed_status)
else
log.debug(log_header .. "reactor status length mismatch")
valid = false
@ -966,29 +905,37 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.boilers) == "table" then
local boil_sum = 0
computed_status = BLR_STATE.OFFLINE
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[id] == nil then
unit.boiler_ps_tbl[id].publish("computed_status", computed_status)
local connected = rtu_statuses.boilers[id] ~= nil
unit.rtu_hw.boilers[id].connected = connected
if not connected then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
end
for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then
local data = unit.boiler_data_tbl[id]
local ps = unit.boiler_ps_tbl[id]
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.rtu_hw.boilers[id].faulted = rtu_faulted
if rtu_faulted then
computed_status = BLR_STATE.FAULT
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
boil_sum = boil_sum + data.state.boil_rate
computed_status = util.trinary(data.state.boil_rate > 0, BLR_STATE.ACTIVE, BLR_STATE.IDLE)
else computed_status = BLR_STATE.UNFORMED end
unit.boiler_ps_tbl[id].publish("computed_status", computed_status)
if data.state.boil_rate > 0 then
ps.publish("computed_status", 5) -- active
else
ps.publish("computed_status", 4) -- idle
end
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid boiler id ", id))
valid = false
@ -1005,36 +952,39 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.turbines) == "table" then
local flow_sum = 0
computed_status = TRB_STATE.OFFLINE
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[id] == nil then
unit.turbine_ps_tbl[id].publish("computed_status", computed_status)
local connected = rtu_statuses.turbines[id] ~= nil
unit.rtu_hw.turbines[id].connected = connected
if not connected then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
end
for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then
local data = unit.turbine_data_tbl[id]
local ps = unit.turbine_ps_tbl[id]
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.rtu_hw.turbines[id].faulted = rtu_faulted
if rtu_faulted then
computed_status = TRB_STATE.FAULT
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
flow_sum = flow_sum + data.state.flow_rate
if data.tanks.energy_fill >= 0.99 then
computed_status = TRB_STATE.TRIPPED
ps.publish("computed_status", 6) -- trip
elseif data.state.flow_rate < 100 then
computed_status = TRB_STATE.IDLE
ps.publish("computed_status", 4) -- idle
else
computed_status = TRB_STATE.ACTIVE
ps.publish("computed_status", 5) -- active
end
else computed_status = TRB_STATE.UNFORMED end
unit.turbine_ps_tbl[id].publish("computed_status", computed_status)
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid turbine id ", id))
valid = false
@ -1049,34 +999,33 @@ function iocontrol.update_unit_statuses(statuses)
-- dynamic tank statuses
if type(rtu_statuses.tanks) == "table" then
computed_status = TNK_STATE.OFFLINE
for id = 1, #unit.tank_ps_tbl do
if rtu_statuses.tanks[id] == nil then
unit.tank_ps_tbl[id].publish("computed_status", computed_status)
-- disconnected
unit.tank_ps_tbl[id].publish("computed_status", 1)
end
end
for id, tank in pairs(rtu_statuses.tanks) do
if type(unit.tank_data_tbl[id]) == "table" then
local data = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local data = unit.tank_data_tbl[id] ---@type dynamicv_session_db
local ps = unit.tank_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(tank, data, ps)
if rtu_faulted then
computed_status = TNK_STATE.FAULT
ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.fill >= 0.99 then
computed_status = TNK_STATE.HIGH_FILL
ps.publish("computed_status", 6) -- full
elseif data.tanks.fill < 0.20 then
computed_status = TNK_STATE.LOW_FILL
ps.publish("computed_status", 5) -- low
else
computed_status = TNK_STATE.ONLINE
ps.publish("computed_status", 4) -- on-line
end
else computed_status = TNK_STATE.UNFORMED end
unit.tank_ps_tbl[id].publish("computed_status", computed_status)
else
ps.publish("computed_status", 2) -- not formed
end
else
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
valid = false
@ -1109,12 +1058,9 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.envds) == "table" then
local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
unit.rad_monitors = {}
for id, envd in pairs(rtu_statuses.envds) do
local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
for _, envd in pairs(rtu_statuses.envds) do
local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number
any_conn = true
@ -1122,10 +1068,6 @@ function iocontrol.update_unit_statuses(statuses)
max_rad = rad_raw
max_reading = radiation
end
if not rtu_faulted then
unit.rad_monitors[id] = { radiation = radiation, raw = rad_raw }
end
end
if any_conn then
@ -1148,7 +1090,6 @@ function iocontrol.update_unit_statuses(statuses)
unit.annunciator = status[3]
if type(unit.annunciator) ~= "table" then
---@diagnostic disable-next-line: missing-fields
unit.annunciator = {}
log.debug(log_header .. "annunciator state not a table")
valid = false
@ -1203,19 +1144,15 @@ function iocontrol.update_unit_statuses(statuses)
if type(unit_state) == "table" then
if #unit_state == 8 then
unit.status_lines[1] = unit_state[1]
unit.status_lines[2] = unit_state[2]
unit.auto_ready = unit_state[3]
unit.auto_degraded = unit_state[4]
unit.waste_mode = unit_state[5]
unit.waste_product = unit_state[6]
unit.last_rate_change_ms = unit_state[7]
unit.turbine_flow_stable = unit_state[8]
unit.unit_ps.publish("U_StatusLine1", unit.status_lines[1])
unit.unit_ps.publish("U_StatusLine2", unit.status_lines[2])
unit.unit_ps.publish("U_AutoReady", unit.auto_ready)
unit.unit_ps.publish("U_AutoDegraded", unit.auto_degraded)
unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2])
unit.unit_ps.publish("U_AutoReady", unit_state[3])
unit.unit_ps.publish("U_AutoDegraded", unit_state[4])
unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
@ -1232,7 +1169,7 @@ function iocontrol.update_unit_statuses(statuses)
local valve_states = status[6]
if type(valve_states) == "table" then
if #valve_states == 6 then
if #valve_states == 5 then
unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0)
unit.unit_ps.publish("V_pu_state", valve_states[1] == 2)
unit.unit_ps.publish("V_po_conn", valve_states[2] > 0)
@ -1243,8 +1180,6 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("V_am_state", valve_states[4] == 2)
unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0)
unit.unit_ps.publish("V_emc_state", valve_states[5] == 2)
unit.unit_ps.publish("V_aux_conn", valve_states[6] > 0)
unit.unit_ps.publish("V_aux_state", valve_states[6] == 2)
else
log.debug(log_header .. "valve states length mismatch")
valid = false
@ -1262,7 +1197,6 @@ function iocontrol.update_unit_statuses(statuses)
local u_spent_rate = waste_rate
local u_pu_rate = util.trinary(is_pu, waste_rate, 0.0)
local u_po_rate = unit.sna_out_rate
local u_po_pl_rate = 0
unit.unit_ps.publish("pu_rate", u_pu_rate)
unit.unit_ps.publish("po_rate", u_po_rate)
@ -1273,7 +1207,6 @@ function iocontrol.update_unit_statuses(statuses)
u_spent_rate = u_po_rate
unit.unit_ps.publish("po_pl_rate", u_po_rate)
unit.unit_ps.publish("po_am_rate", 0)
u_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
u_spent_rate = 0
@ -1285,8 +1218,6 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("po_am_rate", 0)
end
unit.waste_stats = { u_pu_rate, u_po_rate, u_po_pl_rate }
unit.unit_ps.publish("ws_rate", u_spent_rate)
pu_rate = pu_rate + u_pu_rate
@ -1295,8 +1226,6 @@ function iocontrol.update_unit_statuses(statuses)
end
end
io.facility.waste_stats = { burn_rate_sum, pu_rate, po_rate, po_pl_rate, po_am_rate, spent_rate }
io.facility.ps.publish("burn_sum", burn_rate_sum)
io.facility.ps.publish("sna_count", sna_count_sum)
io.facility.ps.publish("pu_rate", pu_rate)

View File

@ -25,17 +25,17 @@ local pctl = {
control_states = {
---@class sys_auto_config
process = {
mode = PROCESS.INACTIVE, ---@type PROCESS
mode = PROCESS.INACTIVE,
burn_target = 0.0,
charge_target = 0.0,
gen_target = 0.0,
limits = {}, ---@type number[]
waste_product = PRODUCT.PLUTONIUM, ---@type WASTE_PRODUCT
limits = {},
waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false,
sps_low_power = false
},
waste_modes = {}, ---@type WASTE_MODE[]
priority_groups = {} ---@type AUTO_GROUP[]
waste_modes = {},
priority_groups = {}
},
commands = {
unit = {}, ---@type process_command_state[][]
@ -46,10 +46,9 @@ local pctl = {
---@class process_command_state
---@field active boolean if this command is live
---@field timeout integer expiration time of this command request
---@field requestors function[] list of callbacks from the requestors
---@field requestors table list of callbacks from the requestors
-- write auto process control to config file
---@return boolean saved
local function _write_auto_config()
-- save config
settings.set("ControlStates", pctl.control_states)
@ -61,8 +60,6 @@ local function _write_auto_config()
return saved
end
--#region Core
-- initialize the process controller
---@param iocontrol ioctl iocontrl system
---@param coord_comms coord_comms coordinator communications
@ -83,8 +80,8 @@ function process.init(iocontrol, coord_comms)
ctl_proc.limits[i] = 0.1
end
local ctrl_states = settings.get("ControlStates", {}) ---@type sys_control_states
local config = ctrl_states.process
local ctrl_states = settings.get("ControlStates", {})
local config = ctrl_states.process ---@type sys_auto_config
-- facility auto control configuration
if type(config) == "table" then
@ -106,7 +103,7 @@ function process.init(iocontrol, coord_comms)
pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
local unit = pctl.io.units[id]
local unit = pctl.io.units[id] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
end
@ -119,7 +116,7 @@ function process.init(iocontrol, coord_comms)
end
-- unit waste states
local waste_modes = ctrl_states.waste_modes
local waste_modes = ctrl_states.waste_modes ---@type table|nil
if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do
pctl.control_states.waste_modes[id] = mode
@ -130,7 +127,7 @@ function process.init(iocontrol, coord_comms)
end
-- unit priority groups
local prio_groups = ctrl_states.priority_groups
local prio_groups = ctrl_states.priority_groups ---@type table|nil
if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do
pctl.control_states.priority_groups[id] = group
@ -139,11 +136,6 @@ function process.init(iocontrol, coord_comms)
log.info("PROCESS: loaded priority groups settings")
end
-- report to the supervisor all initial configuration data has been sent
-- startup resume can occur if needed
local p = ctl_proc
pctl.comms.send_ready(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
end
-- create a handle to process control for usage of commands that get acknowledgements
@ -188,36 +180,6 @@ function process.create_handle()
end
end
-- start automatic process control with current settings
function handle.process_start()
if f_request(F_CMD.START, handle.fac_ack.on_start) then
local p = pctl.control_states.process
pctl.comms.send_auto_start(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
log.debug("PROCESS: START AUTO CTRL")
end
end
-- start automatic process control with remote settings that haven't been set on the coordinator
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function handle.process_start_remote(mode, burn_target, charge_target, gen_target, limits)
if f_request(F_CMD.START, handle.fac_ack.on_start) then
pctl.comms.send_auto_start(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: START AUTO CTRL")
end
end
-- stop process control
function handle.process_stop()
if f_request(F_CMD.STOP, handle.fac_ack.on_stop) then
pctl.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTRL")
end
end
handle.fac_ack = {}
-- luacheck: no unused args
@ -232,16 +194,6 @@ function process.create_handle()
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_ack_alarms(success) end
-- facility auto control start ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_start(success) end
-- facility auto control stop ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_stop(success) end
-- luacheck: unused args
--#endregion
@ -291,7 +243,6 @@ function process.create_handle()
handle.unit_ack = {}
for u = 1, pctl.io.facility.num_units do
---@diagnostic disable-next-line: missing-fields
handle.unit_ack[u] = {}
---@class process_unit_ack
@ -343,14 +294,6 @@ function process.clear_timed_out()
end
end
-- get the control states table
---@nodiscard
function process.get_control_states() return pctl.control_states end
--#endregion
--#region Command Handling
-- handle a command acknowledgement
---@param cmd_state process_command_state
---@param success boolean if the command was successful
@ -392,21 +335,6 @@ function process.set_rate(id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- assign a unit to a group
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
pctl.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then
log.error("process.set_group(): failed to save coordinator settings file")
end
end
-- set waste mode
---@param id integer unit ID
---@param mode integer waste mode
@ -441,39 +369,81 @@ function process.reset_alarm(id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
-- assign a unit to a group
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
pctl.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then
log.error("process.set_group(): failed to save coordinator settings file")
end
end
--#endregion
--------------------------
-- AUTO PROCESS CONTROL --
--------------------------
-- start automatic process control
function process.start_auto()
pctl.comms.send_auto_start(pctl.control_states.process)
log.debug("PROCESS: START AUTO CTL")
end
-- stop automatic process control
function process.stop_auto()
pctl.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTL")
end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save
pctl.control_states.process.waste_product = product
_write_auto_config()
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save
pctl.control_states.process.pu_fallback = enabled
_write_auto_config()
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
pctl.control_states.process.sps_low_power = enabled
_write_auto_config()
end
-- save process control settings
---@param mode PROCESS process control mode
---@param mode PROCESS control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param charge_target number charge target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
---@param limits table unit burn rate limits
function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE")
@ -502,7 +472,9 @@ function process.start_ack_handle(response)
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
ctl_proc.limits[i] = response[6][i]
pctl.io.units[i].unit_ps.publish("burn_limit", ctl_proc.limits[i])
local unit = pctl.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
end
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
@ -510,41 +482,28 @@ function process.start_ack_handle(response)
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
_write_auto_config()
process.fac_ack(F_CMD.START, ack)
pctl.io.facility.start_ack(ack)
end
-- record waste product settting after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product settting
function process.waste_ack_handle(response)
-- update config table and save
pctl.control_states.process.waste_product = response
_write_auto_config()
pctl.io.facility.ps.publish("process_waste_product", response)
end
-- record plutonium fallback settting after attempting to change it
---@param response boolean supervisor plutonium fallback settting
function process.pu_fb_ack_handle(response)
-- update config table and save
pctl.control_states.process.pu_fallback = response
_write_auto_config()
pctl.io.facility.ps.publish("process_pu_fallback", response)
end
-- record SPS low power settting after attempting to change it
---@param response boolean supervisor SPS low power settting
function process.sps_lp_ack_handle(response)
-- update config table and save
pctl.control_states.process.sps_low_power = response
_write_auto_config()
pctl.io.facility.ps.publish("process_sps_low_power", response)
end
--#endregion
return process

View File

@ -19,7 +19,7 @@ local unit_view = require("coordinator.ui.layout.unit_view")
local core = require("graphics.core")
local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.DisplayBox")
local DisplayBox = require("graphics.elements.displaybox")
local log_render = coordinator.log_render
@ -30,20 +30,20 @@ local renderer = {}
local engine = {
color_mode = 1, ---@type COLOR_MODE
monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type Window|nil
dmesg_window = nil, ---@type table|nil
ui_ready = false,
fp_ready = false,
ui = {
front_panel = nil, ---@type DisplayBox|nil
main_display = nil, ---@type DisplayBox|nil
flow_display = nil, ---@type DisplayBox|nil
unit_displays = {} ---@type (DisplayBox|nil)[]
front_panel = nil, ---@type graphics_element|nil
main_display = nil, ---@type graphics_element|nil
flow_display = nil, ---@type graphics_element|nil
unit_displays = {}
},
disable_flow_view = false
}
-- init a display to the "default", but set text scale to 0.5
---@param monitor Monitor monitor
---@param monitor table monitor
local function _init_display(monitor)
monitor.setTextScale(0.5)
monitor.setTextColor(colors.white)
@ -64,7 +64,7 @@ local function _init_display(monitor)
end
-- print out that the monitor is too small
---@param monitor Monitor monitor
---@param monitor table monitor
local function _print_too_small(monitor)
monitor.setCursorPos(1, 1)
monitor.setBackgroundColor(colors.black)
@ -137,7 +137,7 @@ function renderer.try_start_fp()
if not engine.fp_ready then
-- show front panel view on terminal
status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end)
@ -275,7 +275,7 @@ function renderer.fp_ready() return engine.fp_ready end
function renderer.ui_ready() return engine.ui_ready end
-- handle a monitor peripheral being disconnected
---@param device Monitor monitor
---@param device table monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_disconnect(device)
local is_used = false
@ -326,7 +326,7 @@ end
-- handle a monitor peripheral being reconnected
---@param name string monitor name
---@param device Monitor monitor
---@param device table monitor
---@return boolean is_used if the monitor is one of the configured monitors
function renderer.handle_reconnect(name, device)
local is_used = false
@ -373,7 +373,7 @@ function renderer.handle_resize(name)
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 Monitor
local device = engine.monitors.main ---@type table
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)
@ -416,7 +416,7 @@ function renderer.handle_resize(name)
end
else engine.dmesg_window.redraw() end
elseif engine.monitors.flow_name == name and engine.monitors.flow then
local device = engine.monitors.flow ---@type Monitor
local device = engine.monitors.flow ---@type table
-- this is necessary if the bottom left block was broken and on reconnect
_init_display(device)

View File

@ -13,7 +13,7 @@ local self = {
nic = nil, ---@type nic
config = nil, ---@type crd_config
next_id = 0,
sessions = {} ---@type pkt_session_struct[]
sessions = {}
}
-- PRIVATE FUNCTIONS --
@ -129,7 +129,7 @@ end
---@param timer_event number
function apisessions.check_all_watchdogs(timer_event)
for i = 1, #self.sessions do
local session = self.sessions[i]
local session = self.sessions[i] ---@type pkt_session_struct
if session.open then
local triggered = session.instance.check_wd(timer_event)
if triggered then
@ -143,7 +143,7 @@ end
-- iterate all the API sessions
function apisessions.iterate_all()
for i = 1, #self.sessions do
local session = self.sessions[i]
local session = self.sessions[i] ---@type pkt_session_struct
if session.open and session.instance.iterate() then
_api_handle_outq(session)
@ -168,7 +168,7 @@ end
-- close all open connections
function apisessions.close_all()
for i = 1, #self.sessions do
local session = self.sessions[i]
local session = self.sessions[i] ---@type pkt_session_struct
if session.open then _shutdown(session) end
end

View File

@ -1,7 +1,6 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
@ -15,9 +14,6 @@ local MGMT_TYPE = comms.MGMT_TYPE
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
local AUTO_GROUP = types.AUTO_GROUP
local WASTE_MODE = types.WASTE_MODE
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000
@ -112,20 +108,14 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
-- link callback transmissions
local f_ack = self.proc_handle.fac_ack
f_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
f_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
f_ack.on_start = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, success }) end
f_ack.on_stop = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.STOP, success }) end
self.proc_handle.fac_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
self.proc_handle.fac_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
for u = 1, iocontrol.get_db().facility.num_units do
local u_ack = self.proc_handle.unit_ack[u]
u_ack.on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
u_ack.on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
u_ack.on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
u_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
self.proc_handle.unit_ack[u].on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
self.proc_handle.unit_ack[u].on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
self.proc_handle.unit_ack[u].on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
self.proc_handle.unit_ack[u].on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
end
-- handle a packet
@ -157,39 +147,13 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
log.info(log_tag .. "FAC SCRAM ALL")
self.proc_handle.fac_scram()
elseif cmd == FAC_COMMAND.STOP then
log.info(log_tag .. "STOP PROCESS CTRL")
self.proc_handle.process_stop()
elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then
log.info(log_tag .. "START PROCESS CTRL")
self.proc_handle.process_start_remote(pkt.data[2], pkt.data[3], pkt.data[4], pkt.data[5], pkt.data[6])
else
log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
log.info(log_tag .. "FAC ACK ALL ALARMS")
self.proc_handle.fac_ack_alarms()
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET WASTE ", pkt.data[2]))
process.set_process_waste(pkt.data[2])
else
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_PU_FB then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET PU FALLBACK ", pkt.data[2]))
process.set_pu_fallback(pkt.data[2] == true)
else
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_SPS_LP then
if pkt.length == 2 then
log.info(util.c(log_tag, " SET SPS LOW POWER ", pkt.data[2]))
process.set_sps_low_power(pkt.data[2] == true)
else
log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
end
else
log.debug(log_tag .. "CRDN facility command unknown")
end
@ -214,33 +178,19 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
self.proc_handle.reset_rps(uid)
elseif cmd == UNIT_COMMAND.SET_BURN then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") then
if pkt.length == 3 then
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
process.set_rate(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= WASTE_MODE.AUTO) and (pkt.data[3] <= WASTE_MODE.MANUAL_ANTI_MATTER) then
log.info(util.c(log_tag, "UNIT[", id, "] SET WASTE ", pkt.data[3]))
process.set_unit_waste(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
end
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
self.proc_handle.ack_all_alarms(uid)
elseif cmd == UNIT_COMMAND.ACK_ALARM then
elseif cmd == UNIT_COMMAND.RESET_ALARM then
elseif cmd == UNIT_COMMAND.SET_GROUP then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
log.info(util.c(log_tag, "UNIT[", uid, "] SET GROUP ", pkt.data[3]))
process.set_group(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit set group missing option")
end
else
log.debug(log_tag .. "CRDN unit command unknown")
end
@ -260,67 +210,20 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
util.table_len(fac.tank_data_tbl),
fac.induction_data_tbl[1] ~= nil, ---@fixme this means nothing
fac.sps_data_tbl[1] ~= nil ---@fixme this means nothing
fac.induction_data_tbl[1] ~= nil,
fac.sps_data_tbl[1] ~= nil,
}
_send(CRDN_TYPE.API_GET_FAC, data)
elseif pkt.type == CRDN_TYPE.API_GET_FAC_DTL then
local fac = db.facility
local mtx_sps = fac.induction_ps_tbl[1]
local units = {}
local tank_statuses = {}
for i = 1, #db.units do
local u = db.units[i]
units[i] = { u.connected, u.annunciator, u.reactor_data, u.tank_data_tbl }
for t = 1, #u.tank_ps_tbl do table.insert(tank_statuses, u.tank_ps_tbl[t].get("computed_status")) end
end
for i = 1, #fac.tank_ps_tbl do table.insert(tank_statuses, fac.tank_ps_tbl[i].get("computed_status")) end
local matrix_data = {
mtx_sps.get("eta_string"),
mtx_sps.get("avg_charge"),
mtx_sps.get("avg_inflow"),
mtx_sps.get("avg_outflow"),
mtx_sps.get("is_charging"),
mtx_sps.get("is_discharging"),
mtx_sps.get("at_max_io")
}
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.auto_scram,
fac.ascram_status,
tank_statuses,
fac.tank_data_tbl,
fac.induction_ps_tbl[1].get("computed_status") or types.IMATRIX_STATE.OFFLINE,
fac.induction_data_tbl[1],
matrix_data,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
fac.sps_data_tbl[1],
units
}
_send(CRDN_TYPE.API_GET_FAC_DTL, data)
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
if pkt.length == 1 and type(pkt.data[1]) == "number" then
local u = db.units[pkt.data[1]]
local statuses = { u.unit_ps.get("computed_status") }
for i = 1, #u.boiler_ps_tbl do table.insert(statuses, u.boiler_ps_tbl[i].get("computed_status")) end
for i = 1, #u.turbine_ps_tbl do table.insert(statuses, u.turbine_ps_tbl[i].get("computed_status")) end
for i = 1, #u.tank_ps_tbl do table.insert(statuses, u.tank_ps_tbl[i].get("computed_status")) end
local u = db.units[pkt.data[1]] ---@type ioctl_unit
if u then
local data = {
u.unit_id,
u.connected,
statuses,
u.rtu_hw,
u.a_group,
u.alarms,
u.annunciator,
@ -335,105 +238,6 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
_send(CRDN_TYPE.API_GET_UNIT, data)
end
end
elseif pkt.type == CRDN_TYPE.API_GET_CTRL then
local data = {}
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.connected,
u.reactor_data.rps_tripped,
u.reactor_data.mek_status.status,
u.reactor_data.mek_status.temp,
u.reactor_data.mek_status.burn_rate,
u.reactor_data.mek_status.act_burn_rate,
u.reactor_data.mek_struct.max_burn,
u.annunciator.AutoControl,
u.a_group
}
end
_send(CRDN_TYPE.API_GET_CTRL, data)
elseif pkt.type == CRDN_TYPE.API_GET_PROC then
local data = {}
local fac = db.facility
local proc = process.get_control_states().process
-- unit data
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.reactor_data.mek_status.status,
u.reactor_data.mek_struct.max_burn,
proc.limits[i],
u.auto_ready,
u.auto_degraded,
u.annunciator.AutoControl,
u.a_group
}
end
-- facility data
data[#db.units + 1] = {
fac.status_lines,
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
fac.auto_scram,
fac.ascram_status,
{ proc.mode, proc.burn_target, proc.charge_target, proc.gen_target }
}
_send(CRDN_TYPE.API_GET_PROC, data)
elseif pkt.type == CRDN_TYPE.API_GET_WASTE then
local data = {}
local fac = db.facility
local proc = process.get_control_states().process
-- unit data
for i = 1, #db.units do
local u = db.units[i]
data[i] = {
u.waste_mode,
u.waste_product,
u.num_snas,
u.sna_peak_rate,
u.sna_max_rate,
u.sna_out_rate,
u.waste_stats
}
end
local process_rate = 0
if fac.sps_data_tbl[1].state then
process_rate = fac.sps_data_tbl[1].state.process_rate
end
-- facility data
data[#db.units + 1] = {
fac.auto_current_waste_product,
fac.auto_pu_fallback_active,
fac.auto_sps_disabled,
proc.waste_product,
proc.pu_fallback,
proc.sps_low_power,
fac.waste_stats,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
process_rate
}
_send(CRDN_TYPE.API_GET_WASTE, data)
elseif pkt.type == CRDN_TYPE.API_GET_RAD then
local data = {}
for i = 1, #db.units do data[i] = db.units[i].rad_monitors end
data[#db.units + 1] = db.facility.rad_monitors
_send(CRDN_TYPE.API_GET_RAD, data)
else
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end

View File

@ -9,7 +9,7 @@ local log = require("scada-common.log")
local sounder = {}
local alarm_ctl = {
speaker = nil, ---@type Speaker
speaker = nil,
volume = 0.5,
stream = audio.new_stream()
}
@ -24,7 +24,7 @@ local function play()
end
-- initialize the annunciator alarm system
---@param speaker Speaker speaker peripheral
---@param speaker table speaker peripheral
---@param volume number speaker volume
function sounder.init(speaker, volume)
alarm_ctl.speaker = speaker
@ -36,7 +36,7 @@ function sounder.init(speaker, volume)
end
-- reconnect the speaker peripheral
---@param speaker Speaker speaker peripheral
---@param speaker table speaker peripheral
function sounder.reconnect(speaker)
alarm_ctl.speaker = speaker
alarm_ctl.playing = false
@ -44,7 +44,7 @@ function sounder.reconnect(speaker)
end
-- set alarm tones
---@param states { [TONE]: boolean } alarm tone commands from supervisor
---@param states table alarm tone commands from supervisor
function sounder.set(states)
-- set tone states
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end

View File

@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.6.16"
local COORDINATOR_VERSION = "v1.5.8"
local CHUNK_LOAD_DELAY_S = 30.0
@ -152,7 +152,7 @@ local function main()
-- core coordinator devices
crd_dev = {
modem = ppm.get_wireless_modem(),
speaker = ppm.get_device("speaker") ---@type Speaker|nil
speaker = ppm.get_device("speaker")
},
-- system objects

View File

@ -24,8 +24,7 @@ local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = {
START_MAIN_UI = 1,
CLOSE_MAIN_UI = 2
START_MAIN_UI = 1
}
local MQ__RENDER_DATA = {
@ -69,7 +68,6 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
-- we only really care if this is our wireless modem
-- if it is another modem, handle other peripheral losses separately
if nic.is_modem(device) then
@ -82,7 +80,7 @@ function threads.thread__main(smem)
nic.connect(other_modem)
else
-- close out main UI
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
renderer.close_ui()
-- alert user to status
log_sys("awaiting comms modem reconnect...")
@ -93,10 +91,8 @@ function threads.thread__main(smem)
log_sys("non-comms modem disconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
elseif type == "speaker" then
---@cast device Speaker
log_sys("lost alarm sounder speaker")
iocontrol.fp_has_speaker(false)
end
@ -106,7 +102,6 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then
if type == "modem" then
---@cast device Modem
if device.isWireless() and not nic.is_connected() then
-- reconnected modem
log_sys("comms modem reconnected")
@ -118,10 +113,8 @@ function threads.thread__main(smem)
log_sys("wired modem reconnected")
end
elseif type == "monitor" then
---@cast device Monitor
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
elseif type == "speaker" then
---@cast device Speaker
log_sys("alarm sounder speaker reconnected")
sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
@ -168,9 +161,9 @@ function threads.thread__main(smem)
-- supervisor watchdog timeout
log_comms("supervisor server timeout")
-- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- close connection, main UI, and stop sounder
coord_comms.close()
renderer.close_ui()
sounder.stop()
else
-- a non-clock/main watchdog timer event
@ -189,9 +182,9 @@ function threads.thread__main(smem)
if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection")
-- close main UI, connection, and stop sounder
smem.q.mq_render.push_command(MQ__RENDER_CMD.CLOSE_MAIN_UI)
-- close connection, main UI, and stop sounder
coord_comms.close()
renderer.close_ui()
sounder.stop()
end
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
@ -303,13 +296,6 @@ function threads.thread__render(smem)
else
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
end
elseif msg.message == MQ__RENDER_CMD.CLOSE_MAIN_UI then
-- close the main UI if it has been drawn
if renderer.ui_ready() then
log_render("closing main UI...")
renderer.close_ui()
log_render("main UI closed")
end
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data

View File

@ -4,18 +4,18 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair
local border = core.border
-- new boiler view
---@param root Container parent
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface

View File

@ -6,15 +6,15 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair
local border = core.border
@ -22,12 +22,13 @@ local border = core.border
local ALIGN = core.ALIGN
-- new induction matrix view
---@param root Container parent
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param data imatrix_session_db matrix data
---@param ps psil ps interface
---@param id number? matrix ID
local function new_view(root, x, y, ps, id)
local function new_view(root, x, y, data, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
@ -93,7 +94,6 @@ local function new_view(root, x, y, ps, id)
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
local function calc_saturation(val)
local data = db.facility.induction_data_tbl[id or 1]
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
return val / data.build.transfer_cap
else return 0 end
@ -105,7 +105,46 @@ local function new_view(root, x, y, ps, id)
local eta = TextBox{parent=rect,x=11,y=20,width=20,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
eta.register(ps, "eta_string", eta.set_value)
eta.register(ps, "eta_ms", function (eta_ms)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
eta.set_value(str)
end)
end
return new_view

View File

@ -8,17 +8,17 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local ALIGN = core.ALIGN
local cpair = core.cpair
-- create a pocket list entry
---@param parent ListBox parent
---@param parent graphics_element parent
---@param id integer PKT session ID
local function init(parent, id)
local s_hi_box = style.fp_theme.highlight_box
@ -28,10 +28,8 @@ local function init(parent, id)
local ps = iocontrol.get_db().fp.ps
local term_w, _ = term.getSize()
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright}
local ps_prefix = "pkt_" .. id .. "_"
@ -45,9 +43,9 @@ local function init(parent, id)
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)

View File

@ -8,20 +8,20 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad")
local StateIndicator = require("graphics.elements.indicators.state")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local Checkbox = require("graphics.elements.controls.Checkbox")
local HazardButton = require("graphics.elements.controls.HazardButton")
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
local RadioButton = require("graphics.elements.controls.RadioButton")
local Checkbox = require("graphics.elements.controls.checkbox")
local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local ALIGN = core.ALIGN
@ -33,7 +33,7 @@ local bw_fg_bg = style.bw_fg_bg
local period = core.flasher.PERIOD
-- new process control view
---@param root Container parent
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
@ -94,14 +94,14 @@ local function new_view(root, x, y)
main.line_break()
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local matrix_flt = IndicatorLight{parent=main,label="Induction Matrix Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS}
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
matrix_flt.register(facility.ps, "as_matrix_fault", matrix_flt.update)
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
@ -131,7 +131,7 @@ local function new_view(root, x, y)
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=s_hi_box}
local b_target = NumericSpinbox{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_target,x=18,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
@ -142,7 +142,7 @@ local function new_view(root, x, y)
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=s_hi_box}
local c_target = NumericSpinbox{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=chg_target,x=18,y=2,text="M"..db.energy_label,fg_bg=style.theme.label_fg}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="M"..db.energy_label,commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
@ -153,7 +153,7 @@ local function new_view(root, x, y)
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=s_hi_box}
local g_target = NumericSpinbox{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=gen_target,x=18,y=2,text="k"..db.energy_label.."/t",fg_bg=style.theme.label_fg}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="k"..db.energy_label.."/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
@ -177,7 +177,7 @@ local function new_view(root, x, y)
local cur_lu = style.theme.disabled
if i <= facility.num_units then
unit = units[i]
unit = units[i] ---@type ioctl_unit
tag_fg_bg = cpair(colors.black, colors.lightBlue)
lim_fg_bg = s_hi_box
label_fg = style.theme.label_fg
@ -191,7 +191,7 @@ local function new_view(root, x, y)
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=s_hi_box}
local lim = NumericSpinbox{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg}
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled,fg_bg=lim_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,fg_bg=label_fg}
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
@ -234,7 +234,7 @@ local function new_view(root, x, y)
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(ind_red.fgd,ind_off),flash=true,period=period.BLINK_250_MS}
if i <= facility.num_units then
local unit = units[i]
local unit = units[i] ---@type ioctl_unit
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
@ -264,22 +264,24 @@ local function new_view(root, x, y)
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()), limits)
process.save(mode.get_value(), b_target.get_value(),
db.energy_convert_to_fe(c_target.get_value()),
db.energy_convert_to_fe(g_target.get_value()),
limits)
end
-- start automatic control after saving process control settings
local function _start_auto()
_save_cfg()
db.process.process_start()
process.start_auto()
end
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=db.process.process_stop,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.red,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg}
db.process.fac_ack.on_start = start.on_response
db.process.fac_ack.on_stop = stop.on_response
facility.start_ack = start.on_response
facility.stop_ack = stop.on_response
function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end)
@ -321,11 +323,11 @@ local function new_view(root, x, y)
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
for i = 1, facility.num_units do
local unit = units[i]
local unit = units[i] ---@type ioctl_unit
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
@ -339,11 +341,11 @@ local function new_view(root, x, y)
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)

View File

@ -6,18 +6,18 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local HorizontalBar = require("graphics.elements.indicators.hbar")
local StateIndicator = require("graphics.elements.indicators.state")
local cpair = core.cpair
local border = core.border
-- create new reactor view
---@param root Container parent
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface

View File

@ -4,19 +4,19 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local DataIndicator = require("graphics.elements.indicators.data")
local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
local cpair = core.cpair
local border = core.border
-- new turbine view
---@param root Container parent
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param ps psil ps interface

View File

@ -11,23 +11,23 @@ local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local AlarmLight = require("graphics.elements.indicators.AlarmLight")
local CoreMap = require("graphics.elements.indicators.CoreMap")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local AlarmLight = require("graphics.elements.indicators.alight")
local CoreMap = require("graphics.elements.indicators.coremap")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local VerticalBar = require("graphics.elements.indicators.vbar")
local HazardButton = require("graphics.elements.controls.HazardButton")
local MultiButton = require("graphics.elements.controls.MultiButton")
local NumericSpinbox = require("graphics.elements.controls.NumericSpinbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local HazardButton = require("graphics.elements.controls.hazard_button")
local MultiButton = require("graphics.elements.controls.multi_button")
local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local AUTO_GROUP = types.AUTO_GROUP
@ -42,7 +42,7 @@ local gry_wht = style.gray_white
local period = core.flasher.PERIOD
-- create a unit view
---@param parent Container parent
---@param parent graphics_element parent
---@param id integer
local function init(parent, id)
local s_hi_box = style.theme.highlight_box
@ -62,7 +62,7 @@ local function init(parent, id)
local ind_wht = style.ind_wht
local db = iocontrol.get_db()
local unit = db.units[id]
local unit = db.units[id] ---@type ioctl_unit
local f_ps = db.facility.ps
local main = Div{parent=parent,x=1,y=1}
@ -361,7 +361,7 @@ local function init(parent, id)
----------------------
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=s_hi_box}
local burn_rate = NumericSpinbox{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=arrow_fg_bg,arrow_disable=style.theme.disabled}
TextBox{parent=burn_control,x=9,y=2,text="mB/t",fg_bg=style.theme.label_fg}
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
@ -381,10 +381,12 @@ local function init(parent, id)
db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
local function start_button_en_check()
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
end
start.register(u_ps, "status", start_button_en_check)
@ -398,7 +400,7 @@ local function init(parent, id)
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)

View File

@ -2,27 +2,22 @@
-- Basic Unit Flow Overview
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.PipeNetwork")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox")
local Rectangle = require("graphics.elements.Rectangle")
local Rectangle = require("graphics.elements.rectangle")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local TriIndicatorLight = require("graphics.elements.indicators.TriIndicatorLight")
local COOLANT_TYPE = types.COOLANT_TYPE
local IndicatorLight = require("graphics.elements.indicators.light")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local ALIGN = core.ALIGN
@ -36,12 +31,12 @@ local wh_gray = style.wh_gray
local lg_gray = style.lg_gray
-- make a new unit flow window
---@param parent Container parent
---@param parent graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param wide boolean whether to render wide version
---@param unit_id integer unit index
local function make(parent, x, y, wide, unit_id)
---@param unit ioctl_unit unit database entry
local function make(parent, x, y, wide, unit)
local s_field = style.theme.field_box
local text_c = style.text_colors
@ -53,13 +48,7 @@ local function make(parent, x, y, wide, unit_id)
local height = 16
local facility = iocontrol.get_db().facility
local unit = iocontrol.get_db().units[unit_id]
local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 6)
local v_start = 1 + ((unit.unit_id - 1) * 5)
local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" }
local v_names = {
@ -91,32 +80,21 @@ local function make(parent, x, y, wide, unit_id)
local rc_pipes = {}
local emc_x = 42 -- emergency coolant connection x point
if unit.num_boilers > 0 then
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
end
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
else
table.insert(rc_pipes, pipe(0, 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
end
emc_x = 3
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
end
if unit.has_tank then
local is_water = tank_types[tank_conns[unit_id]] == COOLANT_TYPE.WATER
-- emergency coolant connection x point
local emc_x = util.trinary(is_water and (unit.num_boilers > 0), 42, 3)
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, util.trinary(is_water, colors.blue, colors.lightBlue), true, true))
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
end
local prv_yo = math.max(3 - unit.num_turbines, 0)
@ -179,12 +157,12 @@ local function make(parent, x, y, wide, unit_id)
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.cyan, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.green, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
@ -232,21 +210,17 @@ local function make(parent, x, y, wide, unit_id)
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=8,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.2f",value=0,width=17}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root

View File

@ -10,16 +10,16 @@ local reactor_view = require("coordinator.ui.components.reactor")
local boiler_view = require("coordinator.ui.components.boiler")
local turbine_view = require("coordinator.ui.components.turbine")
local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.PipeNetwork")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox")
local ALIGN = core.ALIGN
local pipe = core.pipe
-- make a new unit overview window
---@param parent Container parent
---@param parent graphics_element parent
---@param x integer top left x
---@param y integer top left y
---@param unit ioctl_unit unit database entry

View File

@ -13,18 +13,17 @@ local unit_flow = require("coordinator.ui.components.unit_flow")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local PipeNetwork = require("graphics.elements.PipeNetwork")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local HorizontalBar = require("graphics.elements.indicators.hbar")
local IndicatorLight = require("graphics.elements.indicators.light")
local StateIndicator = require("graphics.elements.indicators.state")
local CONTAINER_MODE = types.CONTAINER_MODE
local COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN
@ -35,7 +34,7 @@ local pipe = core.pipe
local wh_gray = style.wh_gray
-- create new flow view
---@param main DisplayBox main displaybox
---@param main graphics_element main displaybox
local function init(main)
local s_hi_bright = style.theme.highlight_box_bright
local s_field = style.theme.field_box
@ -46,10 +45,8 @@ local function init(main)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local tank_defs = facility.tank_defs
local tank_conns = facility.tank_conns
local tank_list = facility.tank_list
local tank_types = facility.tank_fluid_types
local tank_defs = facility.tank_defs
local tank_list = facility.tank_list
-- window header message
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
@ -59,16 +56,12 @@ local function init(main)
datetime.register(facility.ps, "date_time", datetime.set_value)
local po_pipes = {}
local emcool_pipes = {}
local water_pipes = {}
-- get the y offset for this unit index
---@param idx integer unit index
local function y_ofs(idx) return ((idx - 1) * 20) end
-- get the coolant color
---@param idx integer tank index
local function c_clr(idx) return util.trinary(tank_types[tank_conns[idx]] == COOLANT_TYPE.WATER, colors.blue, colors.lightBlue) end
-- determinte facility tank start/end from the definitions list
---@param start_idx integer start index of table iteration
---@param end_idx integer end index of table iteration
@ -88,13 +81,12 @@ local function init(main)
for i = 1, facility.num_units do
if units[i].has_tank then
local y = y_ofs(i)
local color = c_clr(i)
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
local u = units[i] ---@type ioctl_unit
local x = util.trinary(u.num_boilers == 0, 45, 84)
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
end
end
else
@ -102,17 +94,17 @@ local function init(main)
for i = 1, #tank_defs do
if tank_defs[i] > 0 then
local y = y_ofs(i)
local color = c_clr(i)
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(1, y, 21, y, color, true))
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
else
table.insert(emcool_pipes, pipe(2, y, 2, y + 3, color, true))
table.insert(emcool_pipes, pipe(2, y, 21, y, color, true))
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
end
local x = util.trinary((tank_types[tank_conns[i]] == COOLANT_TYPE.SODIUM) or (units[i].num_boilers == 0), 45, 84)
table.insert(emcool_pipes, pipe(21, y, x, y + 2, color, true, true))
local u = units[i] ---@type ioctl_unit
local x = util.trinary(u.num_boilers == 0, 45, 84)
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
end
end
@ -122,14 +114,13 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
if i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, c_clr(first_fdef), true))
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, c_clr(first_fdef), true))
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
@ -139,19 +130,17 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 4 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
@ -160,12 +149,12 @@ local function init(main)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if tank_defs[a] == 2 then
table.insert(emcool_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, c_clr(a), true))
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
if tank_defs[b] == 2 then
table.insert(emcool_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), c_clr(a), true))
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
end
elseif tank_defs[b] == 2 then
table.insert(emcool_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, c_clr(b), true))
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
end
end
elseif facility.tank_mode == 4 then
@ -174,19 +163,17 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
@ -196,19 +183,17 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 3 or i == 4 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
@ -218,19 +203,17 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 4 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
@ -240,19 +223,17 @@ local function init(main)
for i = 1, #tank_defs do
local y = y_ofs(i)
local color = c_clr(first_fdef)
if i == 1 or i == 2 then
if tank_defs[i] == 2 then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, c_clr(i), true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
end
elseif i == first_fdef then
table.insert(emcool_pipes, pipe(0, y, 1, y + 5, color, true))
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
elseif i > first_fdef then
if i == last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
elseif i < last_fdef then
table.insert(emcool_pipes, pipe(0, y - 14, 0, y + 5, color, true))
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
end
end
end
@ -260,15 +241,15 @@ local function init(main)
end
local flow_x = 3
if #emcool_pipes > 0 then
if #water_pipes > 0 then
flow_x = 25
PipeNetwork{parent=main,x=2,y=3,pipes=emcool_pipes,bg=style.theme.bg}
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=style.theme.bg}
end
for i = 1, facility.num_units do
local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
util.nop()
end
@ -286,7 +267,7 @@ local function init(main)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),colors=style.ind_grn}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
@ -294,35 +275,6 @@ local function init(main)
end
end
------------------------------
-- auxiliary coolant valves --
------------------------------
for i = 1, facility.num_units do
if units[i].aux_coolant then
local vx
local vy = 3 + y_ofs(i)
if #emcool_pipes == 0 then
vx = util.trinary(units[i].num_boilers == 0, 36, 79)
else
local em_water = tank_types[tank_conns[i]] == COOLANT_TYPE.WATER
vx = util.trinary(units[i].num_boilers == 0, 58, util.trinary(units[i].has_tank and em_water, 94, 91))
end
PipeNetwork{parent=main,x=vx-6,y=vy,pipes={pipe(0,1,9,0,colors.blue,true)},bg=style.theme.bg}
TextBox{parent=main,x=vx,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
TextBox{parent=main,x=vx+5,y=vy,text="\x1b",fg_bg=cpair(colors.blue,text_col.bkg),width=1}
local conn = IndicatorLight{parent=main,x=vx-3,y=vy+1,label=util.sprintf("PV%02d-AUX", i * 6),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=vx-3,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_aux_conn", conn.update)
open.register(units[i].unit_ps, "V_aux_state", open.update)
end
end
-------------------
-- dynamic tanks --
-------------------
@ -351,10 +303,8 @@ local function init(main)
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=s_field}
local is_water = tank_types[i] == COOLANT_TYPE.WATER
TextBox{parent=tank_box,x=2,y=6,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=16}
TextBox{parent=tank_box,x=2,y=6,text="Water Level",width=11,fg_bg=style.label}
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",width=11,fg_bg=style.label}
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}

View File

@ -14,16 +14,16 @@ local pkt_entry = require("coordinator.ui.components.pkt_entry")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local TabBar = require("graphics.elements.controls.TabBar")
local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.LED")
local LEDPair = require("graphics.elements.indicators.LEDPair")
local RGBLED = require("graphics.elements.indicators.RGBLED")
local LED = require("graphics.elements.indicators.led")
local LEDPair = require("graphics.elements.indicators.ledpair")
local RGBLED = require("graphics.elements.indicators.ledrgb")
local LINK_STATE = types.PANEL_LINK_STATE
@ -34,13 +34,11 @@ local cpair = core.cpair
local led_grn = style.led_grn
-- create new front panel view
---@param panel DisplayBox main displaybox
---@param panel graphics_element main displaybox
---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
local page_div = Div{parent=panel,x=1,y=3}
@ -63,7 +61,7 @@ local function init(panel, num_units)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update)
else
@ -133,9 +131,9 @@ local function init(panel, num_units)
-- about footer
--
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -147,8 +145,8 @@ local function init(panel, num_units)
-- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1} -- padding
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
-- assemble page panes

View File

@ -14,14 +14,14 @@ local unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core")
local TextBox = require("graphics.elements.TextBox")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local ALIGN = core.ALIGN
-- create new main view
---@param main DisplayBox main displaybox
---@param main graphics_element main displaybox
local function init(main)
local s_header = style.theme.header
@ -37,8 +37,7 @@ local function init(main)
ping.register(facility.ps, "sv_ping", ping.update)
datetime.register(facility.ps, "date_time", datetime.set_value)
---@type Div, Div, Div, Div
local uo_1, uo_2, uo_3, uo_4
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
local cnc_y_start = 3
local row_1_height = 0
@ -88,7 +87,7 @@ local function init(main)
util.nop()
imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
end
return init

View File

@ -5,7 +5,7 @@
local unit_detail = require("coordinator.ui.components.unit_detail")
-- create a unit view
---@param main DisplayBox main displaybox
---@param main graphics_element main displaybox
---@param id integer
local function init(main, id)
unit_detail(main, id)

View File

@ -8,17 +8,15 @@ local util = require("scada-common.util")
local pgi = {}
local data = {
pkt_list = nil, ---@type ListBox|nil
pkt_entry = nil, ---@type function
pkt_list = nil, ---@type nil|graphics_element
pkt_entry = nil, ---@type function
-- session entries
s_entries = {
pkt = {} ---@type Div[]
}
s_entries = { pkt = {} }
}
-- link list boxes
---@param pkt_list ListBox pocket list element
---@param pkt_entry fun(parent: ListBox, id: integer) : Div pocket entry constructor
---@param pkt_list graphics_element pocket list element
---@param pkt_entry function pocket entry constructor
function pgi.link_elements(pkt_list, pkt_entry)
data.pkt_list = pkt_list
data.pkt_entry = pkt_entry

View File

@ -2,20 +2,16 @@
-- Graphics Style Options
--
local util = require("scada-common.util")
local util = require("scada-common.util")
local core = require("graphics.core")
local themes = require("graphics.themes")
local coordinator = require("coordinator.coordinator")
local core = require("graphics.core")
local themes = require("graphics.themes")
---@class crd_style
local style = {}
local cpair = core.cpair
local config = coordinator.config
-- front panel styling
style.fp_theme = themes.sandstone
@ -151,110 +147,236 @@ style.gray_white = cpair(colors.gray, colors.white)
-- UI COMPONENTS --
style.reactor = {
-- reactor states<br>
---@see REACTOR_STATE
-- reactor states
states = {
{ color = cpair(colors.black, colors.yellow), text = "PLC OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "PLC FAULT" },
{ color = cpair(colors.white, colors.gray), text = "DISABLED" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" },
{ color = cpair(colors.black, colors.red), text = "SCRAMMED" },
{ color = cpair(colors.black, colors.red), text = "FORCE DISABLED" }
{
color = cpair(colors.black, colors.yellow),
text = "PLC OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "PLC FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "DISABLED"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "SCRAMMED"
},
{
color = cpair(colors.black, colors.red),
text = "FORCE DISABLED"
}
}
}
style.boiler = {
-- boiler states<br>
---@see BOILER_STATE
-- boiler states
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
style.turbine = {
-- turbine states<br>
---@see TURBINE_STATE
-- turbine states
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" },
{ color = cpair(colors.black, colors.red), text = "TRIP" }
}
}
style.dtank = {
-- dynamic tank states<br>
---@see TANK_STATE
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.black, colors.green), text = "ONLINE" },
{ color = cpair(colors.black, colors.yellow), text = "LOW FILL" },
{ color = cpair(colors.black, colors.green), text = "FILLED" }
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
},
{
color = cpair(colors.black, colors.red),
text = "TRIP"
}
}
}
style.imatrix = {
-- induction matrix states<br>
---@see IMATRIX_STATE
-- induction matrix states
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.black, colors.green), text = "ONLINE" },
{ color = cpair(colors.black, colors.yellow), text = "LOW CHARGE" },
{ color = cpair(colors.black, colors.yellow), text = "HIGH CHARGE" }
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.black, colors.green),
text = "ONLINE"
},
{
color = cpair(colors.black, colors.yellow),
text = "LOW CHARGE"
},
{
color = cpair(colors.black, colors.yellow),
text = "HIGH CHARGE"
}
}
}
style.sps = {
-- SPS states<br>
---@see SPS_STATE
-- SPS states
states = {
{ color = cpair(colors.black, colors.yellow), text = "OFF-LINE" },
{ color = cpair(colors.black, colors.orange), text = "NOT FORMED" },
{ color = cpair(colors.black, colors.orange), text = "RTU FAULT" },
{ color = cpair(colors.white, colors.gray), text = "IDLE" },
{ color = cpair(colors.black, colors.green), text = "ACTIVE" }
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.white, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
-- get waste styling, which depends on the configuration
---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: { text: string, fg_bg: cpair, active_fg_bg:cpair } }
function style.get_waste()
local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
style.dtank = {
-- dynamic tank states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
},
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.black, colors.green),
text = "ONLINE"
},
{
color = cpair(colors.black, colors.yellow),
text = "LOW FILL"
},
{
color = cpair(colors.black, colors.green),
text = "FILLED"
},
}
}
return {
-- auto waste processing states
states = {
{ color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
{ color = cpair(colors.black, po_color), text = "POLONIUM" },
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
style.waste = {
-- auto waste processing states
states = {
{
color = cpair(colors.black, colors.green),
text = "PLUTONIUM"
},
states_abbrv = {
{ color = cpair(colors.black, pu_color), text = "Pu" },
{ color = cpair(colors.black, po_color), text = "Po" },
{ color = cpair(colors.black, colors.purple), text = "AM" }
{
color = cpair(colors.black, colors.cyan),
text = "POLONIUM"
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) },
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) },
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) },
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
{
color = cpair(colors.black, colors.purple),
text = "ANTI MATTER"
}
},
states_abbrv = {
{
color = cpair(colors.black, colors.green),
text = "Pu"
},
{
color = cpair(colors.black, colors.cyan),
text = "Po"
},
{
color = cpair(colors.black, colors.purple),
text = "AM"
}
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.green)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
}
end
}
return style

View File

@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "2.4.8"
core.version = "2.3.4"
core.flasher = flasher
core.events = events
@ -17,8 +17,6 @@ core.events = events
---@enum ALIGN
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
---@alias Container DisplayBox|Div|ListBox|MultiPane|AppMultiPane|Rectangle
---@class graphics_border
---@field width integer
---@field color color
@ -126,10 +124,10 @@ end
-- Interactive Field Manager
---@param e graphics_base element
---@param max_len integer max value length
---@param fg_bg cpair enabled fg/bg
---@param dis_fg_bg? cpair disabled fg/bg
---@param align_right? boolean true to align content right while unfocused
---@param max_len any max value length
---@param fg_bg any enabled fg/bg
---@param dis_fg_bg any disabled fg/bg
---@param align_right any true to align content right while unfocused
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
local self = {
frame_start = 1,

View File

@ -2,6 +2,7 @@
-- Generic Graphics Element
--
-- local log = require("scada-common.log")
local util = require("scada-common.util")
local core = require("graphics.core")
@ -10,9 +11,9 @@ local events = core.events
local element = {}
---@class graphics_args
---@field window? Window base window to use, only root elements should use this
---@field parent? graphics_element parent element, if not a root element
---@class graphics_args_generic
---@field window? table
---@field parent? graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer next line if omitted
@ -23,6 +24,47 @@ local element = {}
---@field hidden? boolean true to hide on initial draw
---@field can_focus? boolean true if this element can be focused, false by default
---@alias graphics_args graphics_args_generic
---|waiting_args
---|app_button_args
---|checkbox_args
---|hazard_button_args
---|multi_button_args
---|push_button_args
---|radio_2d_args
---|radio_button_args
---|sidebar_args
---|spinbox_args
---|switch_button_args
---|tabbar_args
---|number_field_args
---|text_field_args
---|alarm_indicator_light
---|core_map_args
---|data_indicator_args
---|hbar_args
---|icon_indicator_args
---|indicator_led_args
---|indicator_led_pair_args
---|indicator_led_rgb_args
---|indicator_light_args
---|power_indicator_args
---|rad_indicator_args
---|signal_bar_args
---|state_indicator_args
---|tristate_indicator_light_args
---|vbar_args
---|app_multipane_args
---|colormap_args
---|displaybox_args
---|div_args
---|listbox_args
---|multipane_args
---|pipenet_args
---|rectangle_args
---|textbox_args
---|tiling_args
---@class element_subscription
---@field ps psil ps used
---@field key string data key
@ -50,14 +92,14 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
is_root = args.parent == nil,
elem_type = debug.getinfo(2).name,
define_completed = false,
p_window = nil, ---@type Window
p_window = nil, ---@type table
position = events.new_coord_2d(1, 1),
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
offset_x = 0,
offset_y = 0,
next_y = 1, -- next child y coordinate
next_id = 1, -- next child ID
subscriptions = {}, ---@type { ps: psil, key: string, func: function }[]
next_id = 0, -- next child ID
subscriptions = {},
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
focused = false,
mt = {}
@ -67,13 +109,13 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
local protected = {
enabled = true,
value = nil, ---@type any
window = nil, ---@type Window
content_window = nil, ---@type Window|nil
window = nil, ---@type table
content_window = nil, ---@type table|nil
mouse_window_shift = { x = 0, y = 0 },
fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1),
children = {}, ---@type graphics_base[]
child_id_map = {} ---@type { [element_id]: integer }
children = {},
child_id_map = {}
}
-- element as string
@ -86,9 +128,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
setmetatable(public, self.mt)
------------------------------
--#region PRIVATE FUNCTIONS --
------------------------------
-----------------------
-- PRIVATE FUNCTIONS --
-----------------------
-- use tab to jump to the next focusable field
---@param reverse boolean
@ -126,10 +168,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
end
---@param children graphics_base[]
---@param children table
local function traverse(children)
for i = 1, #children do
local child = children[i]
local child = children[i] ---@type graphics_base
handle_element(child.get())
if child.get().is_visible() then traverse(child.children) end
end
@ -149,11 +191,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
end
--#endregion
--------------------------------
--#region PROTECTED FUNCTIONS --
--------------------------------
-------------------------
-- PROTECTED FUNCTIONS --
-------------------------
-- prepare the template
---@param offset_x integer x offset for mouse events
@ -246,29 +286,24 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- alias functions
-- window set cursor position<br>
---@see Window.setCursorPos
-- window set cursor position
---@param x integer
---@param y integer
function protected.w_set_cur(x, y) protected.window.setCursorPos(x, y) end
-- set background color<br>
---@see Window.setBackgroundColor
-- set background color
---@param c color
function protected.w_set_bkg(c) protected.window.setBackgroundColor(c) end
-- set foreground (text) color<br>
---@see Window.setTextColor
-- set foreground (text) color
---@param c color
function protected.w_set_fgd(c) protected.window.setTextColor(c) end
-- write text<br>
---@see Window.write
-- write text
---@param str string
function protected.w_write(str) protected.window.write(str) end
-- blit text<br>
---@see Window.blit
-- blit text
---@param str string
---@param fg string
---@param bg string
@ -300,10 +335,8 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- report completion of element instantiation and get the public interface
---@nodiscard
---@param redraw? boolean true to call redraw as part of completing this element
---@return graphics_element element, element_id id
function protected.complete(redraw)
if redraw then protected.redraw() end
function protected.complete()
if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
return public, self.id
end
@ -319,7 +352,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- focus this element and take away focus from all other elements
function protected.take_focus() args.parent.__focus_child(public) end
--#region Action Handlers
-- action handlers --
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg
@ -368,12 +401,14 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
function protected.handle_paste(text) end
-- handle data value changes
---@param ... any value(s)
---@vararg any value(s)
function protected.on_update(...) end
--#endregion
-- callback on control press responses
---@param result any
function protected.response_callback(result) end
--#region Accessors and Control
-- accessors and control --
-- get value
---@nodiscard
@ -392,11 +427,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
function protected.set_max(max) end
-- custom recolor command, varies by element if implemented
---@param ... cpair|color color(s)
---@vararg cpair|color color(s)
function protected.recolor(...) end
-- custom resize command, varies by element if implemented
---@param ... integer sizing
---@vararg integer sizing
function protected.resize(...) end
-- luacheck: pop
@ -411,13 +446,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- stop animations
function protected.stop_anim() end
--#endregion
--#endregion
------------------
--#region SETUP --
------------------
-----------
-- SETUP --
-----------
-- get the parent window
self.p_window = args.window
@ -436,11 +467,9 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
self.id = args.parent.__add_child(args.id, protected)
end
--#endregion
-----------------------------
--#region PUBLIC FUNCTIONS --
-----------------------------
----------------------
-- PUBLIC FUNCTIONS --
----------------------
-- get the window object
---@nodiscard
@ -475,14 +504,16 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
if args.parent ~= nil then
-- remove self from parent
-- log.debug("removing " .. self.id .. " from parent")
args.parent.__remove_child(self.id)
else
-- log.debug("no parent for " .. self.id .. " on delete attempt")
end
end
--#region ELEMENT TREE
-- ELEMENT TREE --
-- add a child element
---@package
---@nodiscard
---@param key string|nil id
---@param child graphics_base
@ -492,24 +523,20 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
self.next_y = child.frame.y + child.frame.h
local id = key ---@type element_id|nil
local id = key ---@type string|integer|nil
if id == nil then
id = self.next_id
self.next_id = self.next_id + 1
end
-- see #539 on GitHub
-- using #protected.children after inserting may give the wrong index, since if it inserts in a hole that completes the list then
-- the length will jump up to the full length of the list, possibly making two map entries point to the same child
protected.child_id_map[id] = #protected.children + 1
table.insert(protected.children, child)
protected.child_id_map[id] = #protected.children
return id
end
-- remove a child element
---@package
---@param id element_id id
function public.__remove_child(id)
local index = protected.child_id_map[id]
@ -521,13 +548,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
-- actions to take upon a child element becoming ready (initial draw/construction completed)
---@package
---@param key element_id id
---@param child graphics_element
function public.__child_ready(key, child) protected.on_added(key, child) end
-- focus solely on this child
---@package
---@param child graphics_element
function public.__focus_child(child)
if self.is_root then
@ -537,7 +562,6 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
-- a child was focused, used to make sure it is actually visible to the user in the content frame
---@package
---@param child graphics_element
function public.__child_focused(child)
protected.on_child_focused(child)
@ -547,8 +571,8 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- get a child element
---@nodiscard
---@param id element_id
---@return graphics_element element
function public.get_child(id) return ({ protected.children[protected.child_id_map[id]].get() })[1] end
---@return graphics_element
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
-- get all children
---@nodiscard
@ -574,7 +598,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- remove all child elements and reset next y
function public.remove_all()
for i = 1, #protected.children do
local child = protected.children[i].get() ---@type graphics_element
local child = protected.children[i].get() ---@type graphics_element
child.delete()
protected.on_removed(child.get_id())
end
@ -595,33 +619,29 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
local elem = child.get().get_element_by_id(id)
if elem ~= nil then return elem end
end
else return ({ protected.children[index].get() })[1] end
else return protected.children[index].get() end
end
--#endregion
--#region AUTO-PLACEMENT
-- AUTO-PLACEMENT --
-- skip a line for automatically placed elements
function public.line_break()
self.next_y = self.next_y + 1
end
--#endregion
-- PROPERTIES --
--#region PROPERTIES
-- get element ID
-- get element id
---@nodiscard
---@return element_id
function public.get_id() return self.id end
-- get element relative x position
-- get element x
---@nodiscard
---@return integer x
function public.get_x() return protected.frame.x end
-- get element relative y position
-- get element y
---@nodiscard
---@return integer y
function public.get_y() return protected.frame.y end
@ -641,12 +661,12 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
---@return cpair fg_bg
function public.get_fg_bg() return protected.fg_bg end
-- get the element's value
-- get the element value
---@nodiscard
---@return any value
function public.get_value() return protected.get_value() end
-- set the element's value
-- set the element value
---@param value any new value
function public.set_value(value) protected.set_value(value) end
@ -708,11 +728,11 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
-- custom recolor command, varies by element if implemented
---@param ... cpair|color color(s)
---@vararg cpair|color color(s)
function public.recolor(...) protected.recolor(...) end
-- resize attributes of the element value if supported
---@param ... number dimensions (element specific)
---@vararg number dimensions (element specific)
function public.resize(...) protected.resize(...) end
-- reposition the element window<br>
@ -736,9 +756,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
self.bounds.y2 = self.position.y + protected.frame.h - 1
end
--#endregion
--#region FUNCTION CALLBACKS
-- FUNCTION CALLBACKS --
-- handle a monitor touch or mouse click if this element is visible
---@param event mouse_interaction mouse interaction event
@ -800,9 +818,13 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
-- draw the element given new data
---@param ... any new data
---@vararg any new data
function public.update(...) protected.on_update(...) end
-- on a control request response
---@param result any
function public.on_response(result) protected.response_callback(result) end
-- register a callback with a PSIL, allowing for automatic unregister on delete<br>
-- do not use graphics elements directly with PSIL subscribe()
---@param ps psil PSIL to subscribe to
@ -813,9 +835,7 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
ps.subscribe(key, func)
end
--#endregion
--#region VISIBILITY & ANIMATIONS
-- VISIBILITY & ANIMATIONS --
-- check if this element is visible
function public.is_visible() return protected.window.isVisible() end
@ -829,7 +849,6 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
-- hide the element and disables animations<br>
-- this alone does not cause an element to be fully hidden, it only prevents updates from being shown<br>
---@see Window.redraw
---@see graphics_element.redraw
---@see graphics_element.content_redraw
---@param clear? boolean true to visibly hide this element (redraws the parent)
@ -881,10 +900,6 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
end
end
--#endregion
--#endregion
return protected
end

View File

@ -12,10 +12,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new waiting animation element.
-- new waiting animation element
---@param args waiting_args
---@return Waiting element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function waiting(args)
local state = 0
local run_animation = false
@ -23,7 +23,7 @@ return function (args)
args.height = 3
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
local blit_fg = e.fg_bg.blit_fgd
local blit_bg = e.fg_bg.blit_bkg
@ -103,8 +103,7 @@ return function (args)
e.start_anim()
---@class Waiting:graphics_element
local Waiting, id = e.complete()
return Waiting, id
return e.complete()
end
return waiting

View File

@ -24,15 +24,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new app multipane container element.
-- new app multipane element
---@nodiscard
---@param args app_multipane_args
---@return AppMultiPane element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function multipane(args)
element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 1
@ -100,8 +100,10 @@ return function (args)
end
end
---@class AppMultiPane:graphics_element
local AppMultiPane, id = e.complete(true)
-- initial draw
e.redraw()
return AppMultiPane, id
return e.complete()
end
return multipane

View File

@ -9,10 +9,10 @@ local element = require("graphics.element")
---@field y? integer auto incremented if omitted
---@field hidden? boolean true to hide on initial draw
-- Create a horizontal reference color map. Primarily used for tuning custom colors.
-- new color map
---@param args colormap_args
---@return ColorMap element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function colormap(args)
local bkg = "008877FFCCEE114455DD9933BBAA2266"
local spaces = string.rep(" ", 32)
@ -20,7 +20,7 @@ return function (args)
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- draw color map
function e.redraw()
@ -28,8 +28,10 @@ return function (args)
e.w_blit(spaces, bkg, bkg)
end
---@class ColorMap:graphics_element
local ColorMap, id = e.complete(true)
-- initial draw
e.redraw()
return ColorMap, id
return e.complete()
end
return colormap

View File

@ -20,10 +20,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new app icon style button control element, like on a mobile device.
-- new app button
---@param args app_button_args
---@return App element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function app_button(args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.title) == "string", "title is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
@ -33,7 +33,7 @@ return function (args)
args.width = 7
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- draw the app button
local function draw()
@ -123,8 +123,10 @@ return function (args)
draw()
end
---@class App:graphics_element
local App, id = e.complete(true)
-- initial draw
e.redraw()
return App, id
return e.complete()
end
return app_button

View File

@ -6,7 +6,6 @@ local element = require("graphics.element")
---@class checkbox_args
---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox
---@field disable_fg_bg? cpair text colors when disabled
---@field default? boolean default value
---@field callback? function function to call on press
---@field parent graphics_element
@ -16,10 +15,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new checkbox control element.
-- new checkbox control
---@param args checkbox_args
---@return Checkbox element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function checkbox(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
@ -28,7 +27,7 @@ return function (args)
args.width = 2 + string.len(args.label)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.default == true
@ -36,27 +35,20 @@ return function (args)
local function draw()
e.w_set_cur(1, 1)
local fgd, bkg = args.box_fg_bg.fgd, args.box_fg_bg.bkg
if (not e.enabled) and type(args.disable_fg_bg) == "table" then
fgd = args.disable_fg_bg.bkg
bkg = args.disable_fg_bg.fgd
end
if e.value then
-- show as selected
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_set_fgd(args.box_fg_bg.bkg)
e.w_set_bkg(args.box_fg_bg.fgd)
e.w_write("\x88")
e.w_set_fgd(fgd)
e.w_set_fgd(args.box_fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
else
-- show as unselected
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(bkg)
e.w_set_bkg(args.box_fg_bg.bkg)
e.w_write("\x88")
e.w_set_fgd(bkg)
e.w_set_fgd(args.box_fg_bg.bkg)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write("\x95")
end
@ -65,18 +57,16 @@ return function (args)
-- write label text
local function draw_label()
if e.enabled and e.is_focused() then
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
elseif (not e.enabled) and type(args.disable_fg_bg) == "table" then
e.w_set_fgd(args.disable_fg_bg.fgd)
e.w_set_bkg(args.disable_fg_bg.bkg)
e.w_write(args.label)
else
e.w_set_cur(3, 1)
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
e.w_write(args.label)
end
e.w_set_cur(3, 1)
e.w_write(args.label)
end
-- handle mouse interaction
@ -108,22 +98,24 @@ return function (args)
draw()
end
-- handle focus
e.on_focused = draw_label
e.on_unfocused = draw_label
-- handle enable
e.on_enabled = draw_label
e.on_disabled = draw_label
-- element redraw
function e.redraw()
draw()
draw_label()
end
-- handle focus
e.on_focused = draw_label
e.on_unfocused = draw_label
-- initial draw
e.redraw()
-- handle enable
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class Checkbox:graphics_element
local Checkbox, id = e.complete(true)
return Checkbox, id
return e.complete()
end
return checkbox

View File

@ -18,10 +18,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new hazard button control element.
-- new hazard button
---@param args hazard_button_args
---@return HazardButton element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function hazard_button(args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.accent) == "number", "accent is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
@ -32,7 +32,7 @@ return function (args)
local timeout = args.timeout or 1.5
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- draw border
---@param accent color accent color
@ -159,6 +159,13 @@ return function (args)
end
end
-- callback on request response
---@param result boolean true for success, false for failure
function e.response_callback(result)
tcd.abort(on_timeout)
if result then on_success() else on_failure(0) end
end
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
@ -191,15 +198,10 @@ return function (args)
draw_border(args.accent)
end
---@class HazardButton:graphics_element
local HazardButton, id = e.complete(true)
-- initial draw
e.redraw()
-- callback for request response
---@param success boolean
function HazardButton.on_response(success)
tcd.abort(on_timeout)
if success then on_success() else on_failure(0) end
end
return HazardButton, id
return e.complete()
end
return hazard_button

View File

@ -25,10 +25,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new multi button control element (latch selection, exclusively one button at a time).
-- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args
---@return MultiButton element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function multi_button(args)
element.assert(type(args.options) == "table", "options is a required field")
element.assert(#args.options > 0, "at least one option is required")
element.assert(type(args.callback) == "function", "callback is a required field")
@ -52,7 +52,7 @@ return function (args)
args.width = (button_width * #args.options) + #args.options + 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- button state (convert nil to 1 if missing)
e.value = args.default or 1
@ -126,8 +126,10 @@ return function (args)
e.redraw()
end
---@class MultiButton:graphics_element
local MultiButton, id = e.complete(true)
-- initial draw
e.redraw()
return MultiButton, id
return e.complete()
end
return multi_button

View File

@ -25,10 +25,10 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new push button control element.
-- new push button
---@param args push_button_args
---@return PushButton element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function push_button(args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
@ -48,7 +48,7 @@ return function (args)
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], constrain)
local e = element.new(args, constrain)
local text_lines = util.strwrap(args.text, e.frame.w)
@ -157,8 +157,10 @@ return function (args)
e.on_focused = show_pressed
e.on_unfocused = show_unpressed
---@class PushButton:graphics_element
local PushButton, id = e.complete(true)
-- initial draw
e.redraw()
return PushButton, id
return e.complete()
end
return push_button

View File

@ -23,10 +23,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new 2-dimensional (rows and columns of options) radio button list control element (latch selection, exclusively one color at a time).
-- new 2D radio button list (latch selection, exclusively one color at a time)
---@param args radio_2d_args
---@return Radio2D element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function radio_2d_button(args)
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
@ -70,7 +70,7 @@ return function (args)
args.height = max_rows
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- selected option (convert nil to 1 if missing)
e.value = args.default or 1
@ -194,8 +194,10 @@ return function (args)
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class Radio2D:graphics_element
local Radio2D, id = e.complete(true)
-- initial draw
e.redraw()
return Radio2D, id
return e.complete()
end
return radio_2d_button

View File

@ -11,7 +11,6 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field options table button options
---@field radio_colors cpair radio button colors (inner & outer)
---@field select_color color color for radio button border when selected
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field default? integer default state, defaults to options[1]
---@field min_width? integer text length + 2 if omitted
---@field callback? function function to call on touch
@ -22,10 +21,10 @@ local KEY_CLICK = core.events.KEY_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new radio button list control element (latch selection, exclusively one button at a time).
-- new radio button list (latch selection, exclusively one button at a time)
---@param args radio_button_args
---@return RadioButton element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function radio_button(args)
element.assert(type(args.options) == "table", "options is a required field")
element.assert(#args.options > 0, "at least one option is required")
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
@ -50,7 +49,7 @@ return function (args)
args.height = #args.options -- one line per option
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
local focused_opt = 1
@ -65,10 +64,6 @@ return function (args)
local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a)
local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
if e.value == i and args.dis_fg_bg and not e.enabled then
outer_color = args.radio_colors.color_a
end
e.w_set_cur(1, i)
e.w_set_fgd(inner_color)
@ -80,14 +75,9 @@ return function (args)
e.w_write("\x95")
-- write button text
if args.dis_fg_bg and not e.enabled then
e.w_set_fgd(args.dis_fg_bg.fgd)
e.w_set_bkg(args.dis_fg_bg.bkg)
elseif i == focused_opt and e.is_focused() then
if e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
end
if i == focused_opt and e.is_focused() and e.enabled then
e.w_set_fgd(e.fg_bg.bkg)
e.w_set_bkg(e.fg_bg.fgd)
else
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
@ -149,8 +139,10 @@ return function (args)
e.on_enabled = e.redraw
e.on_disabled = e.redraw
---@class RadioButton:graphics_element
local RadioButton, id = e.complete(true)
-- initial draw
e.redraw()
return RadioButton, id
return e.complete()
end
return radio_button

View File

@ -17,14 +17,14 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new sidebar tab selector control element.
-- new sidebar tab selector
---@param args sidebar_args
---@return Sidebar element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function sidebar(args)
args.width = 3
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- default to 1st tab
e.value = 1
@ -129,14 +129,8 @@ return function (args)
end
-- update the sidebar navigation options
---@param items sidebar_entry[] sidebar entries
---@param items table sidebar entries
function e.on_update(items)
---@class sidebar_entry
---@field label string
---@field tall boolean
---@field color cpair
---@field callback function|nil
local next_y = 1
tabs = {}
@ -166,8 +160,9 @@ return function (args)
-- element redraw
e.redraw = draw
---@class Sidebar:graphics_element
local Sidebar, id = e.complete(true)
e.redraw()
return Sidebar, id
return e.complete()
end
return sidebar

View File

@ -20,10 +20,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new spinbox control element (minimum value is 0).
-- new spinbox control (minimum value is 0)
---@param args spinbox_args
---@return NumericSpinbox element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function spinbox(args)
-- properties
local digits = {}
local wn_prec = args.whole_num_precision
@ -51,7 +51,7 @@ return function (args)
args.height = 3
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- set initial value
e.value = args.default or 0
@ -179,8 +179,10 @@ return function (args)
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
end
---@class NumericSpinbox:graphics_element
local NumericSpinbox, id = e.complete(true)
-- initial draw
e.redraw()
return NumericSpinbox, id
return e.complete()
end
return spinbox

View File

@ -17,10 +17,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new latching switch button control element.
-- new switch button (latch high/low)
---@param args switch_button_args
---@return SwitchButton element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function switch_button(args)
element.assert(type(args.text) == "string", "text is a required field")
element.assert(type(args.callback) == "function", "callback is a required field")
element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
@ -33,7 +33,7 @@ return function (args)
args.width = math.max(text_width, args.min_width)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.default or false
@ -72,8 +72,10 @@ return function (args)
e.redraw()
end
---@class SwitchButton:graphics_element
local SwitchButton, id = e.complete(true)
-- initial draw
e.redraw()
return SwitchButton, id
return e.complete()
end
return switch_button

View File

@ -23,10 +23,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new tab selector control element.
-- new tab selector
---@param args tabbar_args
---@return TabBar element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function tabbar(args)
element.assert(type(args.tabs) == "table", "tabs is a required field")
element.assert(#args.tabs > 0, "at least one tab is required")
element.assert(type(args.callback) == "function", "callback is a required field")
@ -46,7 +46,7 @@ return function (args)
local button_width = math.max(max_width, args.min_width or 0)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
@ -120,8 +120,10 @@ return function (args)
e.redraw()
end
---@class TabBar:graphics_element
local TabBar, id = e.complete(true)
-- initial draw
e.redraw()
return TabBar, id
return e.complete()
end
return tabbar

View File

@ -13,16 +13,13 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a root display box.
-- new root display box
---@nodiscard
---@param args displaybox_args
---@return DisplayBox element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function displaybox(args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
---@class DisplayBox:graphics_element
local DisplayBox, id = e.complete()
return DisplayBox, id
return element.new(args).complete()
end
return displaybox

View File

@ -13,16 +13,13 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new div container element.
-- new div element
---@nodiscard
---@param args div_args
---@return Div element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function div(args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
---@class Div:graphics_element
local Div, id = e.complete()
return Div, id
return element.new(args).complete()
end
return div

View File

@ -27,10 +27,10 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new numeric entry field.
-- new numeric entry field
---@param args number_field_args
---@return NumberField element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function number_field(args)
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
@ -38,53 +38,14 @@ return function (args)
args.can_focus = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
local has_decimal = false
args.max_chars = args.max_chars or e.frame.w
-- determine the format to convert the number to a string
local format = "%d"
if args.allow_decimal then
if args.max_frac_digits then
format = "%."..args.max_frac_digits.."f"
else format = "%f" end
end
-- set the value to a formatted numeric string<br>
-- trims trailing zeros from floating point numbers
---@param num number
local function _set_value(num)
local str = util.sprintf(format, num)
if args.allow_decimal then
local found_nonzero = false
local str_table = {}
for i = #str, 1, -1 do
local c = string.sub(str, i, i)
if found_nonzero then
str_table[i] = c
else
if c == "." then
found_nonzero = true
elseif c ~= "0" then
str_table[i] = c
found_nonzero = true
end
end
end
e.value = table.concat(str_table)
else
e.value = str
end
end
-- set initial value
_set_value(args.default or 0)
e.value = "" .. (args.default or 0)
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
@ -146,17 +107,7 @@ return function (args)
-- set the value (must be a number)
---@param val number number to show
function e.set_value(val)
local num, max, min = tonumber(val), tonumber(args.max), tonumber(args.min)
if max and num > max then
_set_value(max)
elseif min and num < min then
_set_value(min)
elseif num then
_set_value(num)
end
ifield.set_value(e.value)
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end
end
-- set minimum input value
@ -185,9 +136,11 @@ return function (args)
-- handle unfocused
function e.on_unfocused()
local val, max, min = tonumber(e.value), tonumber(args.max), tonumber(args.min)
local val = tonumber(e.value)
local max = tonumber(args.max)
local min = tonumber(args.min)
if val then
if type(val) == "number" then
if args.max_int_digits or args.max_frac_digits then
local str = e.value
local ceil = false
@ -216,17 +169,17 @@ return function (args)
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
val = tonumber((parts[1] or "") .. parts[2]) or 0
val = tonumber((parts[1] or "") .. parts[2])
end
if max and val > max then
_set_value(max)
if type(args.max) == "number" and val > max then
e.value = "" .. max
ifield.nav_start()
elseif min and val < min then
_set_value(min)
elseif type(args.min) == "number" and val < min then
e.value = "" .. min
ifield.nav_start()
else
_set_value(val)
e.value = "" .. val
ifield.nav_end()
end
else
@ -242,14 +195,10 @@ return function (args)
e.on_disabled = ifield.show
e.redraw = ifield.show
---@class NumberField:graphics_element
local NumberField, id = e.complete(true)
-- initial draw
e.redraw()
-- get the numeric value of this field
---@return number value the value, or 0 if not a valid number
function NumberField.get_numeric()
return tonumber(e.value) or 0
end
return NumberField, id
return e.complete()
end
return number_field

View File

@ -19,15 +19,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new text entry field.
-- new text entry field
---@param args text_field_args
---@return TextField element, element_id id
return function (args)
---@return graphics_element element, element_id id, function censor_ctl
local function text_field(args)
args.height = 1
args.can_focus = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- set initial value
e.value = args.value or ""
@ -95,10 +95,11 @@ return function (args)
e.on_disabled = ifield.show
e.redraw = ifield.show
---@class TextField:graphics_element
local TextField, id = e.complete(true)
-- initial draw
e.redraw()
TextField.censor = ifield.censor
return TextField, id
local elem, id = e.complete()
return elem, id, ifield.censor
end
return text_field

View File

@ -20,11 +20,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new alarm indicator light element.
-- new alarm indicator light
---@nodiscard
---@param args alarm_indicator_light
---@return AlarmLight element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function alarm_indicator_light(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field")
@ -49,7 +49,7 @@ return function (args)
local c3 = colors.toBlit(args.c3)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 1
@ -113,8 +113,10 @@ return function (args)
e.w_write(args.label)
end
---@class AlarmLight:graphics_element
local AlarmLight, id = e.complete(true)
-- initial draw
e.redraw()
return AlarmLight, id
return e.complete()
end
return alarm_indicator_light

View File

@ -13,11 +13,11 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
-- Create a new core map diagram indicator element.
-- new core map box
---@nodiscard
---@param args core_map_args
---@return CoreMap element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function core_map(args)
element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
@ -29,7 +29,7 @@ return function (args)
args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 0
@ -165,8 +165,10 @@ return function (args)
draw_core(e.value)
end
---@class CoreMap:graphics_element
local CoreMap, id = e.complete(true)
-- initial draw
e.redraw()
return CoreMap, id
return e.complete()
end
return core_map

View File

@ -19,11 +19,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create new data indicator element.
-- new data indicator
---@nodiscard
---@param args data_indicator_args
---@return DataIndicator element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function data(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.format) == "string", "format is a required field")
element.assert(args.value ~= nil, "value is a required field")
@ -32,7 +32,7 @@ return function (args)
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.value
@ -94,8 +94,10 @@ return function (args)
e.on_update(e.value)
end
---@class DataIndicator:graphics_element
local DataIndicator, id = e.complete(true)
-- initial draw
e.redraw()
return DataIndicator, id
return e.complete()
end
return data

View File

@ -17,13 +17,13 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new horizontal fill bar indicator element.
-- new horizontal bar
---@nodiscard
---@param args hbar_args
---@return graphics_element element, element_id id
return function (args)
local function hbar(args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 0.0
@ -119,8 +119,10 @@ return function (args)
e.on_update(e.value)
end
---@class HorizontalBar:graphics_element
local HorizontalBar, id = e.complete(true)
-- initial draw
e.redraw()
return HorizontalBar, id
return e.complete()
end
return hbar

View File

@ -18,11 +18,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new icon indicator element.
-- new icon indicator
---@nodiscard
---@param args icon_indicator_args
---@return IconIndicator element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function icon(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.states) == "table", "states is a required field")
@ -30,7 +30,7 @@ return function (args)
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.value or 1
if e.value == true then e.value = 2 end
@ -71,8 +71,10 @@ return function (args)
e.on_update(e.value)
end
---@class IconIndicator:graphics_element
local IconIndicator, id = e.complete(true)
-- initial draw
e.redraw()
return IconIndicator, id
return e.complete()
end
return icon

View File

@ -18,11 +18,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new indicator LED element.
-- new indicator LED
---@nodiscard
---@param args indicator_led_args
---@return LED element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function indicator_led(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field")
@ -36,7 +36,7 @@ return function (args)
local flash_on = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = false
@ -95,8 +95,10 @@ return function (args)
end
end
---@class LED:graphics_element
local LED, id = e.complete(true)
-- initial draw
e.redraw()
return LED, id
return e.complete()
end
return indicator_led

View File

@ -20,12 +20,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new three-state LED indicator light. Two "active" states (colors c1 and c2) and an inactive state (off).<br>
-- Values: 1 = off, 2 = c1, 3 = c2
-- new dual LED indicator light
---@nodiscard
---@param args indicator_led_pair_args
---@return LEDPair element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function indicator_led_pair(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.off) == "number", "off is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field")
@ -45,7 +44,7 @@ return function (args)
local c2 = colors.toBlit(args.c2)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 1
@ -105,8 +104,10 @@ return function (args)
end
end
---@class LEDPair:graphics_element
local LEDPair, id = e.complete(true)
-- initial draw
e.redraw()
return LEDPair, id
return e.complete()
end
return indicator_led_pair

View File

@ -13,11 +13,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new RGB LED indicator light element.
-- new RGB LED indicator light
---@nodiscard
---@param args indicator_led_rgb_args
---@return RGBLED element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function indicator_led_rgb(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field")
@ -25,7 +25,7 @@ return function (args)
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 1
@ -52,8 +52,10 @@ return function (args)
end
end
---@class RGBLED:graphics_element
local RGBLED, id = e.complete(true)
-- initial draw
e.redraw()
return RGBLED, id
return e.complete()
end
return indicator_led_rgb

View File

@ -18,11 +18,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new indicator light element.
-- new indicator light
---@nodiscard
---@param args indicator_light_args
---@return IndicatorLight element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function indicator_light(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.colors) == "table", "colors is a required field")
@ -36,7 +36,7 @@ return function (args)
local flash_on = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = false
@ -93,8 +93,10 @@ return function (args)
e.w_write(args.label)
end
---@class IndicatorLight:graphics_element
local IndicatorLight, id = e.complete(true)
-- initial draw
e.redraw()
return IndicatorLight, id
return e.complete()
end
return indicator_light

View File

@ -19,11 +19,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new power indicator. Variant of a data indicator with dynamic energy units.
-- new power indicator
---@nodiscard
---@param args power_indicator_args
---@return PowerIndicator element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function power(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.unit) == "string", "unit is a required field")
element.assert(type(args.value) == "number", "value is a required field")
@ -32,7 +32,7 @@ return function (args)
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.value
@ -82,8 +82,10 @@ return function (args)
e.on_update(e.value)
end
---@class PowerIndicator:graphics_element
local PowerIndicator, id = e.complete(true)
-- initial draw
e.redraw()
return PowerIndicator, id
return e.complete()
end
return power

View File

@ -19,11 +19,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new radiation indicator element. Variant of a data indicator using dynamic Sievert unit precision.
-- new radiation indicator
---@nodiscard
---@param args rad_indicator_args
---@return RadIndicator element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function rad(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.format) == "string", "format is a required field")
element.assert(util.is_int(args.width), "width is a required field")
@ -31,7 +31,7 @@ return function (args)
args.height = 1
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.value or types.new_zero_radiation_reading()
@ -83,8 +83,10 @@ return function (args)
e.on_update(e.value)
end
---@class RadIndicator:graphics_element
local RadIndicator, id = e.complete(true)
-- initial draw
e.redraw()
return RadIndicator, id
return e.complete()
end
return rad

View File

@ -15,16 +15,16 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality)
---@field hidden? boolean true to hide on initial draw
-- Create a new signal bar indicator element.
-- new signal bar
---@nodiscard
---@param args signal_bar_args
---@return SignalBar element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function signal_bar(args)
args.height = 1
args.width = util.trinary(args.compact, 1, 2)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 0
@ -76,8 +76,10 @@ return function (args)
end
end
---@class SignalBar:graphics_element
local SignalBar, id = e.complete(true)
-- initial draw
e.redraw()
return SignalBar, id
return e.complete()
end
return signal_bar

View File

@ -20,11 +20,11 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new state indicator element.
-- new state indicator
---@nodiscard
---@param args state_indicator_args
---@return StateIndicator element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function state_indicator(args)
element.assert(type(args.states) == "table", "states is a required field")
if util.is_int(args.height) then
@ -52,7 +52,7 @@ return function (args)
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = args.value or 1
@ -74,8 +74,10 @@ return function (args)
---@param val integer indicator state
function e.set_value(val) e.on_update(val) end
---@class StateIndicator:graphics_element
local StateIndicator, id = e.complete(true)
-- initial draw
e.redraw()
return StateIndicator, id
return e.complete()
end
return state_indicator

View File

@ -20,11 +20,11 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new tri-state indicator light element.
-- new tri-state indicator light
---@nodiscard
---@param args tristate_indicator_light_args
---@return TriIndicatorLight element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function tristate_indicator_light(args)
element.assert(type(args.label) == "string", "label is a required field")
element.assert(type(args.c1) == "number", "c1 is a required field")
element.assert(type(args.c2) == "number", "c2 is a required field")
@ -38,7 +38,7 @@ return function (args)
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 1
@ -102,8 +102,10 @@ return function (args)
e.w_write(args.label)
end
---@class TriIndicatorLight:graphics_element
local TriIndicatorLight, id = e.complete(true)
-- initial draw
e.redraw()
return TriIndicatorLight, id
return e.complete()
end
return tristate_indicator_light

View File

@ -15,13 +15,13 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new vertical fill bar indicator element.
-- new vertical bar
---@nodiscard
---@param args vbar_args
---@return VerticalBar element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function vbar(args)
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 0.0
@ -98,8 +98,10 @@ return function (args)
e.redraw()
end
---@class VerticalBar:graphics_element
local VerticalBar, id = e.complete(true)
-- initial draw
e.redraw()
return VerticalBar, id
return e.complete()
end
return vbar

View File

@ -1,5 +1,6 @@
-- Scroll-able List Box Display Graphics Element
-- local log = require("scada-common.log")
local tcd = require("scada-common.tcd")
local core = require("graphics.core")
@ -29,15 +30,15 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field y integer y position
---@field h integer element height
-- Create a new scrollable listbox container element.
-- new listbox element
---@nodiscard
---@param args listbox_args
---@return ListBox element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function listbox(args)
args.can_focus = true
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- create content window for child elements
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
@ -152,6 +153,7 @@ return function (args)
next_y = next_y + item.h + item_pad
item.e.reposition(1, item.y)
item.e.show()
-- log.debug("iterated " .. item.e.get_id())
end
content_height = next_y
@ -210,6 +212,7 @@ return function (args)
---@param child graphics_element child element
function e.on_added(id, child)
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
-- log.debug("added child " .. id .. " into slot " .. #list)
update_positions()
end
@ -219,10 +222,12 @@ return function (args)
for idx, elem in ipairs(list) do
if elem.id == id then
table.remove(list, idx)
-- log.debug("removed child " .. id .. " from slot " .. idx)
update_positions()
return
end
end
-- log.debug("failed to remove child " .. id)
end
-- handle focus
@ -334,8 +339,10 @@ return function (args)
draw_bar()
end
---@class ListBox:graphics_element
local ListBox, id = e.complete(true)
-- initial draw
e.redraw()
return ListBox, id
return e.complete()
end
return listbox

View File

@ -14,15 +14,15 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new multipane container element.
-- new multipane element
---@nodiscard
---@param args multipane_args
---@return MultiPane element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function multipane(args)
element.assert(type(args.panes) == "table", "panes is a required field")
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
e.value = 1
@ -41,8 +41,10 @@ return function (args)
end
end
---@class MultiPane:graphics_element
local MultiPane, id = e.complete(true)
-- initial draw
e.redraw()
return MultiPane, id
return e.complete()
end
return multipane

View File

@ -20,10 +20,10 @@ local element = require("graphics.element")
---@field fg string foreground blit
---@field bg string background blit
-- Create a pipe network diagram.
-- new pipe network
---@param args pipenet_args
---@return PipeNetwork element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function pipenet(args)
element.assert(type(args.pipes) == "table", "pipes is a required field")
args.width = 0
@ -47,7 +47,7 @@ return function (args)
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
-- determine if there are any thin pipes involved
local any_thin = false
@ -322,8 +322,10 @@ return function (args)
if any_thin then map_draw() else vector_draw() end
end
---@class PipeNetwork:graphics_element
local PipeNetwork, id = e.complete(true)
-- initial draw
e.redraw()
return PipeNetwork, id
return e.complete()
end
return pipenet

View File

@ -18,10 +18,10 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new rectangle container element.
-- new rectangle
---@param args rectangle_args
---@return Rectangle element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function rectangle(args)
element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
-- if thin, then width will always need to be 1
@ -45,7 +45,7 @@ return function (args)
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], nil, offset_x, offset_y)
local e = element.new(args, nil, offset_x, offset_y)
-- create content window for child elements
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
@ -191,8 +191,7 @@ return function (args)
e.redraw()
end
---@class Rectangle:graphics_element
local Rectangle, id = e.complete()
return Rectangle, id
return e.complete()
end
return rectangle

View File

@ -10,7 +10,6 @@ local ALIGN = core.ALIGN
---@class textbox_args
---@field text string text to show
---@field alignment? ALIGN text alignment, left by default
---@field trim_whitespace? boolean true to trim whitespace before/after lines of text
---@field anchor? boolean true to use this as an anchor, making it focusable
---@field parent graphics_element
---@field id? string element id
@ -22,10 +21,10 @@ local ALIGN = core.ALIGN
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new text box element.
-- new text box
---@param args textbox_args
---@return TextBox element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function textbox(args)
element.assert(type(args.text) == "string", "text is a required field")
if args.anchor == true then args.can_focus = true end
@ -43,7 +42,7 @@ return function (args)
end
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]], constrain)
local e = element.new(args, constrain)
e.value = args.text
@ -58,11 +57,8 @@ return function (args)
for i = 1, #lines do
if i > e.frame.h then break end
-- trim leading/trailing whitespace, except on the first line
-- leading whitespace on the first line is usually intentional
if args.trim_whitespace == true then
lines[i] = util.trim(lines[i])
end
-- trim leading/trailing whitespace
lines[i] = util.trim(lines[i])
local len = string.len(lines[i])
@ -86,15 +82,10 @@ return function (args)
e.redraw()
end
-- change the foreground color of the text
---@param c color
function e.recolor(c)
e.w_set_fgd(c)
e.redraw()
end
-- initial draw
e.redraw()
---@class TextBox:graphics_element
local TextBox, id = e.complete(true)
return TextBox, id
return e.complete()
end
return textbox

View File

@ -18,14 +18,14 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- Create a new tiling box element.
-- new tiling box
---@param args tiling_args
---@return Tiling element, element_id id
return function (args)
---@return graphics_element element, element_id id
local function tiling(args)
element.assert(type(args.fill_c) == "table", "fill_c is a required field")
-- create new graphics element base object
local e = element.new(args --[[@as graphics_args]])
local e = element.new(args)
local fill_a = args.fill_c.blit_a
local fill_b = args.fill_c.blit_b
@ -52,7 +52,7 @@ return function (args)
element.assert(start_x <= inner_width, "start_x > inner_width")
element.assert(start_y <= inner_height, "start_y > inner_height")
-- draw the tiling box
-- draw tiling box
function e.redraw()
local alternator = true
@ -86,8 +86,10 @@ return function (args)
end
end
---@class Tiling:graphics_element
local Tiling, id = e.complete(true)
-- initial draw
e.redraw()
return Tiling, id
return e.complete()
end
return tiling

View File

@ -18,7 +18,7 @@ local PERIOD = {
flasher.PERIOD = PERIOD
local active = false
local registry = { {}, {}, {} } ---@type [ function[], function[], function [] ] one registry table per period
local registry = { {}, {}, {} } -- one registry table per period
local callback_counter = 0
-- blink registered indicators<br>

View File

@ -1,432 +0,0 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local TextField = require("graphics.elements.form.TextField")
local tri = util.trinary
local cpair = core.cpair
local RIGHT = core.ALIGN.RIGHT
local self = {
importing_legacy = false,
show_auth_key = nil, ---@type function
show_key_btn = nil, ---@type PushButton
auth_key_textbox = nil, ---@type TextBox
auth_key_value = ""
}
local system = {}
-- create the system configuration view
---@param tool_ctl _pkt_cfg_tool_ctl
---@param main_pane MultiPane
---@param cfg_sys [ pkt_config, pkt_config, pkt_config, { [1]: string, [2]: string, [3]: any }[], function ]
---@param divs Div[]
---@param style { [string]: cpair }
---@param exit function
function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local settings_cfg, ini_cfg, tmp_cfg, fields, load_settings = cfg_sys[1], cfg_sys[2], cfg_sys[3], cfg_sys[4], cfg_sys[5]
local ui_cfg, net_cfg, log_cfg, summary = divs[1], divs[2], divs[3], divs[4]
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
--#region Pocket UI
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}}
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
local function submit_ui_opts()
tmp_cfg.GreenPuPellet = pellet_color.get_value() == 1
ui_pane.set_value(2)
end
PushButton{parent=ui_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=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_units()
tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(3)
end
PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#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,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=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,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,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,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,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(2)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,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,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,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,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,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,text="Facility Auth Key"}
local key, _ = 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) key.censor(tri(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,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(4)
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,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,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,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,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
self.importing_legacy = false
tool_ctl.settings_apply.show()
main_pane.set_value(5)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=15,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=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,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 self.importing_legacy then
main_pane.set_value(1)
tool_ctl.viewing_config = false
self.importing_legacy = false
tool_ctl.settings_apply.show()
else
main_pane.set_value(4)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/pocket.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(pellet_color, ini_cfg.GreenPuPellet)
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
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 self.importing_legacy then
self.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}
self.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()self.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_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,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
--#region Tool Functions
-- 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(5)
self.importing_legacy = true
end
-- expose the auth key on the summary page
function self.show_auth_key()
self.show_key_btn.disable()
self.auth_key_textbox.set_value(self.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
self.show_key_btn.enable()
self.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 = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
end
if val == "nil" then val = "<not set>" end
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if (string.len(val) > val_max_w) or string.find(val, "\n") 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}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end
if f[1] == "AuthKey" then self.auth_key_textbox = textbox end
end
end
--#endregion
end
return system

View File

@ -6,18 +6,21 @@ local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local system = require("pocket.config.system")
local core = require("graphics.core")
local themes = require("graphics.themes")
local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local DisplayBox = require("graphics.elements.displaybox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.PushButton")
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
@ -25,12 +28,12 @@ local tri = util.trinary
local cpair = core.cpair
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- changes to the config data/format to let the user know
local changes = {
{ "v0.9.2", { "Added temperature scale options" } },
{ "v0.11.3", { "Added energy scale options" } },
{ "v0.13.2", { "Added option for Po/Pu pellet green/cyan pairing" } }
{ "v0.11.3", { "Added energy scale options" } }
}
---@class pkt_configurator
@ -38,43 +41,49 @@ local configurator = {}
local style = {}
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray)
style.colors = themes.smooth_stone.colors
style.colors = themes.smooth_stone.colors
style.bw_fg_bg = cpair(colors.black, colors.white)
style.g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
style.nav_fg_bg = style.bw_fg_bg
style.btn_act_fg_bg = cpair(colors.white, colors.gray)
style.btn_dis_fg_bg = cpair(colors.lightGray, colors.white)
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)
---@class _pkt_cfg_tool_ctl
local tool_ctl = {
launch_startup = false,
ask_config = false,
has_config = false,
viewing_config = false,
importing_legacy = false,
view_cfg = nil, ---@type PushButton
settings_apply = nil, ---@type PushButton
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
load_legacy = 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 = {
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE
TempScale = 1,
EnergyScale = 1,
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, ---@type LOG_MODE
LogMode = 0,
LogPath = "",
LogDebug = false,
}
@ -86,7 +95,6 @@ local settings_cfg = {}
-- all settings fields, their nice names, and their default values
local fields = {
{ "GreenPuPellet", "Pellet Colors", false },
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 },
@ -114,14 +122,8 @@ local function load_settings(target, raw)
end
-- create the config view
---@param display DisplayBox
---@param display graphics_element
local function config_view(display)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
---@diagnostic disable-next-line: undefined-field
local function exit() os.queueEvent("terminate") end
@ -138,7 +140,7 @@ local function config_view(display)
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,ui_cfg,net_cfg,log_cfg,summary,changelog}}
--#region Main Page
-- Main Page
local y_start = 7
@ -162,33 +164,286 @@ local function config_view(display)
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=btn_dis_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
local function startup()
tool_ctl.launch_startup = true
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(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#region Pocket UI
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_1,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_1,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_1,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts()
tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(3)
end
PushButton{parent=ui_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=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#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,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=net_c_1,x=1,y=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,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,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,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,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(2)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,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,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,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,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,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,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,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(4)
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,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
TextBox{parent=log_c_1,x=1,y=1,text="Configure logging below."}
TextBox{parent=log_c_1,x=1,y=3,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,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,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(5)
else path_err.show() end
end
PushButton{parent=log_c_1,x=1,y=15,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=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,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(4)
end
end
---@param element graphics_element
---@param data any
local function try_set(element, data)
if data ~= nil then element.set_value(data) end
end
local function save_and_continue()
for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/pocket.settings") then
load_settings(settings_cfg, true)
load_settings(ini_cfg)
try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale)
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,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=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}
local start_btn = PushButton{parent=main_page,x=17,y=18,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=2,y=y_start+4,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=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)}
if tool_ctl.ask_config then start_btn.disable() end
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
--#region System Configuration
local settings = { settings_cfg, ini_cfg, tmp_cfg, fields, load_settings }
local divs = { ui_cfg, net_cfg, log_cfg, summary }
system.create(tool_ctl, main_pane, settings, divs, style, exit)
--#endregion
--#region Config Change Log
-- Config Change Log
local cl = Div{parent=changelog,x=2,y=4,width=24}
@ -207,7 +462,87 @@ local function config_view(display)
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}
--#endregion
-- 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(5)
tool_ctl.importing_legacy = true
end
-- expose the auth key on the summary page
function tool_ctl.show_auth_key()
tool_ctl.show_key_btn.disable()
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
end
-- generate the summary list
---@param cfg 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")
elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then
val = util.strval(types.ENERGY_SCALE_NAMES[raw])
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}
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
@ -266,7 +601,7 @@ function configurator.configure(ask_config)
println("configurator error: " .. error)
end
return status, error, tool_ctl.launch_startup
return status, error
end
return configurator

View File

@ -2,11 +2,11 @@
-- I/O Control for Pocket Integration with Supervisor & Coordinator
--
local const = require("scada-common.constants")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
local util = require("scada-common.util")
local iorx = require("pocket.iorx")
local process = require("pocket.process")
local ALARM = types.ALARM
@ -17,6 +17,7 @@ local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS
---@todo nominal trip time is ping (0ms to 10ms usually)
local WARN_TT = 40
local HIGH_TT = 80
@ -34,11 +35,19 @@ iocontrol.LINK_STATE = LINK_STATE
---@class pocket_ioctl
local io = {
version = "unknown", -- pocket version
ps = psil.create(), -- pocket PSIL
loader_require = { sv = false, api = false }
version = "unknown",
ps = psil.create()
}
-- luacheck: no unused args
-- placeholder acknowledge function for type hinting
---@param success boolean
---@diagnostic disable-next-line: unused-local
local function __generic_ack(success) end
-- luacheck: unused args
local config = nil ---@type pkt_config
local comms = nil ---@type pocket_comms
@ -50,8 +59,6 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
comms = pkt_comms
config = cfg
iocontrol.rx = iorx(io)
io.nav = nav
---@class pocket_ioctl_diag
@ -85,22 +92,16 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
get_tone_states = function () comms.diag__get_alarm_tones() end,
tone_buttons = {}, ---@type SwitchButton[]
alarm_buttons = {} ---@type Checkbox[]
ready_warn = nil, ---@type graphics_element
tone_buttons = {},
alarm_buttons = {},
tone_indicators = {} -- indicators to update from supervisor tone states
}
-- computer list
io.diag.get_comps = function () comms.diag__get_computers() end
-- API access
---@class pocket_ioctl_api
io.api = {
get_fac = function () comms.api__get_facility() end,
get_unit = function (unit) comms.api__get_unit(unit) end,
get_ctrl = function () comms.api__get_control() end,
get_proc = function () comms.api__get_process() end,
get_waste = function () comms.api__get_waste() end,
get_rad = function () comms.api__get_rad() end
get_unit = function (unit) comms.api__get_unit(unit) end
}
end
@ -141,14 +142,9 @@ function iocontrol.init_fac(conf)
num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
tank_conns = conf.cooling.fac_tank_conns,
tank_fluid_types = conf.cooling.tank_fluid_types,
all_sys_ok = false,
rtu_count = 0,
status_lines = { "", "" },
auto_ready = false,
auto_active = false,
auto_ramping = false,
@ -157,7 +153,7 @@ function iocontrol.init_fac(conf)
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_fault = false,
matrix_dc = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
@ -167,28 +163,27 @@ function iocontrol.init_fac(conf)
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
auto_sps_disabled = false,
waste_stats = { 0, 0, 0, 0, 0, 0 }, -- waste in, pu, po, po pellets, am, spent waste
radiation = types.new_zero_radiation_reading(),
start_ack = nil, ---@type fun(success: boolean)
stop_ack = nil, ---@type fun(success: boolean)
scram_ack = nil, ---@type fun(success: boolean)
ack_alarms_ack = nil, ---@type fun(success: boolean)
start_ack = __generic_ack,
stop_ack = __generic_ack,
scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
ps = psil.create(),
induction_ps_tbl = {}, ---@type psil[]
induction_data_tbl = {}, ---@type imatrix_session_db[]
induction_ps_tbl = {},
induction_data_tbl = {},
sps_ps_tbl = {}, ---@type psil[]
sps_data_tbl = {}, ---@type sps_session_db[]
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
tank_ps_tbl = {},
tank_data_tbl = {},
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
env_d_ps = psil.create(),
env_d_data = {}
}
-- create induction and SPS tables (currently only 1 of each is supported)
@ -197,6 +192,92 @@ function iocontrol.init_fac(conf)
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
@ -206,23 +287,19 @@ function iocontrol.init_fac(conf)
end
-- create unit data structures
io.units = {} ---@type pioctl_unit[]
io.units = {}
for i = 1, conf.num_units do
---@class pioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = {},
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
status_lines = { "", "" },
auto_ready = false,
auto_degraded = false,
control_state = false,
burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(),
@ -236,7 +313,6 @@ function iocontrol.init_fac(conf)
last_rate_change_ms = 0,
turbine_flow_stable = false,
waste_stats = { 0, 0, 0 }, -- plutonium, polonium, po pellets
-- auto control group
a_group = types.AUTO_GROUP.MANUAL,
@ -247,30 +323,27 @@ function iocontrol.init_fac(conf)
ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
start_ack = nil, ---@type fun(success: boolean)
scram_ack = nil, ---@type fun(success: boolean)
reset_rps_ack = nil, ---@type fun(success: boolean)
ack_alarms_ack = nil, ---@type fun(success: boolean)
start_ack = __generic_ack,
scram_ack = __generic_ack,
reset_rps_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
---@type { [ALARM]: ALARM_STATE }
---@type alarms
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
---@diagnostic disable-next-line: missing-fields
annunciator = {}, ---@type annunciator
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
reactor_data = types.new_reactor_db(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {}, ---@type psil[]
boiler_data_tbl = {}, ---@type boilerv_session_db[]
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {}, ---@type psil[]
turbine_data_tbl = {}, ---@type turbinev_session_db[]
turbine_ps_tbl = {},
turbine_data_tbl = {},
tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
tank_ps_tbl = {},
tank_data_tbl = {}
}
-- on other facility modes, overwrite unit TANK option with facility tank defs
@ -366,6 +439,495 @@ function iocontrol.report_crd_tt(trip_time)
io.ps.publish("crd_conn_quality", state)
end
-- populate facility data from API_GET_FAC
---@param data table
---@return boolean valid
function iocontrol.record_facility_data(data)
local valid = true
local fac = io.facility
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.radiation = data[3]
-- auto control
if type(data[4]) == "table" and #data[4] == 4 then
fac.auto_ready = data[4][1]
fac.auto_active = data[4][2]
fac.auto_ramping = data[4][3]
fac.auto_saturated = data[4][4]
end
-- waste
if type(data[5]) == "table" and #data[5] == 2 then
fac.auto_current_waste_product = data[5][1]
fac.auto_pu_fallback_active = data[5][2]
end
fac.num_tanks = data[6]
fac.has_imatrix = data[7]
fac.has_sps = data[8]
return valid
end
local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end
local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
-- update unit status data from API_GET_UNIT
---@param data table
function iocontrol.record_unit_data(data)
local unit = io.units[data[1]] ---@type pioctl_unit
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.a_group = data[4]
unit.alarms = data[5]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
--#region Annunciator
unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[7]
local control_status = 1
local reactor_status = 1
local reactor_state = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
if unit.reactor_data.rps_trip_cause == "manual" then
reactor_state = 4 -- disabled
rps_status = 3
else
reactor_state = 6 -- SCRAM
rps_status = 2
end
else
rps_status = 4
reactor_state = 4
end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
reactor_state = 5 -- running
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
reactor_state = 3 -- faulted
elseif not unit.reactor_data.formed then
reactor_status = 3
reactor_state = 2 -- not formed
elseif unit.reactor_data.rps_status.force_dis then
reactor_status = 3
reactor_state = 7 -- force disabled
else
reactor_status = 4
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_ReactorStateStatus", reactor_state)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
--#region RTU Devices
unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local boiler_status = 1
local computed_status = 1
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
computed_status = 3
elseif boiler.formed then
boiler_status = 4
if boiler.state.boil_rate > 0 then
computed_status = 5
else
computed_status = 4
end
else
boiler_status = 2
computed_status = 2
end
_record_multiblock_status(unit.rtu_hw.boilers[id].faulted, boiler, ps)
end
ps.publish("BoilerStatus", boiler_status)
ps.publish("BoilerStateStatus", computed_status)
end
unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local turbine_status = 1
local computed_status = 1
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
computed_status = 3
elseif turbine.formed then
turbine_status = 4
if turbine.tanks.energy_fill >= 0.99 then
computed_status = 6
elseif turbine.state.flow_rate < 100 then
computed_status = 4
else
computed_status = 5
end
else
turbine_status = 2
computed_status = 2
end
_record_multiblock_status(unit.rtu_hw.turbines[id].faulted, turbine, ps)
end
ps.publish("TurbineStatus", turbine_status)
ps.publish("TurbineStateStatus", computed_status)
end
unit.tank_data_tbl = data[10]
unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[12]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :)
-- local function red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } end
local function blue(text) return { text = text, color = colors.blue } end
-- unit.reactor_data.rps_status = {
-- high_dmg = false,
-- high_temp = false,
-- low_cool = false,
-- ex_waste = false,
-- ex_hcool = false,
-- no_fuel = false,
-- fault = false,
-- timeout = false,
-- manual = false,
-- automatic = false,
-- sys_fail = false,
-- force_dis = false
-- }
-- if unit.reactor_data.rps_status then
-- for k, v in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end
-- end
if tripped(unit.alarms[ALARM.ContainmentBreach]) then
local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") }
table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items })
end
if tripped(unit.alarms[ALARM.ContainmentRadiation]) then
local items = {
white("RADIATION DETECTED"),
blue("DON HAZMAT SUIT"),
blue("RESOLVE LEAK"),
blue("AWAIT SAFE LEVELS")
}
table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items })
end
if tripped(unit.alarms[ALARM.CriticalDamage]) then
local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") }
table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorLost]) then
local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") }
table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items })
end
if tripped(unit.alarms[ALARM.ReactorDamage]) then
local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") }
table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorOverTemp]) then
local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") }
table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items })
end
if tripped(unit.alarms[ALARM.ReactorHighTemp]) then
local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items})
end
if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then
local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") }
table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items})
end
if tripped(unit.alarms[ALARM.ReactorHighWaste]) then
local items = { blue("CHECK WASTE OUTPUT") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items})
end
if tripped(unit.alarms[ALARM.RPSTransient]) then
local items = {}
local stat = unit.reactor_data.rps_status
-- for k, _ in pairs(stat) do stat[k] = true end
local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end
table.insert(items, white("REACTOR SCRAMMED"))
insert(stat, "high_dmg", "HIGH DAMAGE", colors.red)
insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red)
insert(stat, "low_cool", "CRIT LOW COOLANT")
insert(stat, "ex_waste", "EXCESS WASTE")
insert(stat, "ex_hcool", "EXCESS HEATED COOL")
insert(stat, "no_fuel", "NO FUEL")
insert(stat, "fault", "HARDWARE FAULT")
insert(stat, "timeout", "SUPERVISOR DISCONN")
insert(stat, "manual", "MANUAL SCRAM", colors.white)
insert(stat, "automatic", "AUTOMATIC SCRAM")
insert(stat, "sys_fail", "NOT FORMED", colors.red)
insert(stat, "force_dis", "FORCE DISABLED", colors.red)
table.insert(items, blue("RESOLVE PROBLEM"))
table.insert(items, blue("RESET RPS"))
table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items})
end
if tripped(unit.alarms[ALARM.RCSTransient]) then
local items = {}
local annunc = unit.annunciator
-- for k, v in pairs(annunc) do
-- if type(v) == "boolean" then annunc[k] = true end
-- if type(v) == "table" then
-- for a, _ in pairs(v) do
-- v[a] = true
-- end
-- end
-- end
local function insert(cond, key, text, color)
if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end
end
table.insert(items, white("COOLANT PROBLEM"))
insert(annunc, "RCPTrip", "RCP TRIP", colors.red)
insert(annunc, "CoolantLevelLow", "LOW COOLANT")
if unit.num_boilers == 0 then
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
else
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
end
insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED")
for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end
for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end
for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end
for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end
table.insert(items, blue("CHECK COOLING SYS"))
table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items})
end
if tripped(unit.alarms[ALARM.TurbineTrip]) then
local items = {}
for k, v in ipairs(unit.annunciator.TurbineTrip) do
if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end
end
table.insert(items, blue("CHECK ENERGY OUT"))
table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items})
end
if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then
local items = { blue("CHECK PLC") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items })
end
for k, v in ipairs(unit.annunciator.BoilerOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items})
end
end
for k, v in ipairs(unit.annunciator.TurbineOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items})
end
end
-- if no alarms, put some basic status messages in
if #ecam == 0 then
table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}})
local plural = util.trinary(unit.num_turbines > 1, "S", "")
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
end
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
--#endregion
end
-- get the IO controller database
function iocontrol.get_db() return io end

View File

@ -1,955 +0,0 @@
--
-- I/O Control's Data Receive (Rx) Handlers
--
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local types = require("scada-common.types")
local util = require("scada-common.util")
local DEV_TYPE = comms.DEVICE_TYPE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local BLR_STATE = types.BOILER_STATE
local TRB_STATE = types.TURBINE_STATE
local TNK_STATE = types.TANK_STATE
local MTX_STATE = types.IMATRIX_STATE
local SPS_STATE = types.SPS_STATE
local io ---@type pocket_ioctl
local iorx = {} ---@class iorx
-- populate facility data from API_GET_FAC
---@param data table
---@return boolean valid
function iorx.record_facility_data(data)
local valid = true
local fac = io.facility
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.radiation = data[3]
-- auto control
if type(data[4]) == "table" and #data[4] == 4 then
fac.auto_ready = data[4][1]
fac.auto_active = data[4][2]
fac.auto_ramping = data[4][3]
fac.auto_saturated = data[4][4]
end
-- waste
if type(data[5]) == "table" and #data[5] == 2 then
fac.auto_current_waste_product = data[5][1]
fac.auto_pu_fallback_active = data[5][2]
end
fac.num_tanks = data[6]
fac.has_imatrix = data[7]
fac.has_sps = data[8]
return valid
end
local function tripped(state) return state == ALARM_STATE.TRIPPED or state == ALARM_STATE.ACKED end
local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
if data.build then
for key, val in pairs(data.build) do ps.publish(key, val) end
end
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
-- update unit status data from API_GET_UNIT
---@param data table
function iorx.record_unit_data(data)
local unit = io.units[data[1]]
unit.connected = data[2]
local comp_statuses = data[3]
unit.a_group = data[4]
unit.alarms = data[5]
local next_c_stat = 1
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
--#region Annunciator
unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
local every = true
-- split up online arrays
for id = 1, #val do
every = every and val[id]
if key == "BoilerOnline" then
unit.boiler_ps_tbl[id].publish(key, val[id])
else
unit.turbine_ps_tbl[id].publish(key, val[id])
end
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[7]
local control_status = 1
local reactor_status = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
else
rps_status = 4
end
reactor_status = 4 -- ok, until proven otherwise
-- update reactor/control status
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
elseif (not unit.reactor_data.formed) or unit.reactor_data.rps_status.force_dis then
reactor_status = 3
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.unit_ps.publish(key, val)
end
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_ReactorStateStatus", comp_statuses[next_c_stat])
unit.unit_ps.publish("U_RPS", rps_status)
next_c_stat = next_c_stat + 1
--#endregion
--#region RTU Devices
unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id]
local ps = unit.boiler_ps_tbl[id]
local c_stat = comp_statuses[next_c_stat]
local boiler_status = 1
if c_stat ~= BLR_STATE.OFFLINE then
if c_stat == BLR_STATE.FAULT then
boiler_status = 3
elseif c_stat ~= BLR_STATE.UNFORMED then
boiler_status = 4
else
boiler_status = 2
end
_record_multiblock_status(c_stat == BLR_STATE.FAULT, boiler, ps)
end
ps.publish("BoilerStatus", boiler_status)
ps.publish("BoilerStateStatus", c_stat)
next_c_stat = next_c_stat + 1
end
unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id]
local ps = unit.turbine_ps_tbl[id]
local c_stat = comp_statuses[next_c_stat]
local turbine_status = 1
if c_stat ~= TRB_STATE.OFFLINE then
if c_stat == TRB_STATE.FAULT then
turbine_status = 3
elseif turbine.formed then
turbine_status = 4
else
turbine_status = 2
end
_record_multiblock_status(c_stat == TRB_STATE.FAULT, turbine, ps)
end
ps.publish("TurbineStatus", turbine_status)
ps.publish("TurbineStateStatus", c_stat)
next_c_stat = next_c_stat + 1
end
unit.tank_data_tbl = data[10]
for id = 1, #unit.tank_data_tbl do
local tank = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local c_stat = comp_statuses[next_c_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_c_stat = next_c_stat + 1
end
unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[12]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :)
-- local function red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } end
local function blue(text) return { text = text, color = colors.blue } end
-- if unit.reactor_data.rps_status then
-- for k, _ in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end
-- end
if tripped(unit.alarms[ALARM.ContainmentBreach]) then
local items = { white("REACTOR MELTDOWN"), blue("DON HAZMAT SUIT") }
table.insert(ecam, { color = colors.red, text = "CONTAINMENT BREACH", help = "ContainmentBreach", items = items })
end
if tripped(unit.alarms[ALARM.ContainmentRadiation]) then
local items = {
white("RADIATION DETECTED"),
blue("DON HAZMAT SUIT"),
blue("RESOLVE LEAK"),
blue("AWAIT SAFE LEVELS")
}
table.insert(ecam, { color = colors.red, text = "RADIATION LEAK", help = "ContainmentRadiation", items = items })
end
if tripped(unit.alarms[ALARM.CriticalDamage]) then
local items = { white("MELTDOWN IMMINENT"), blue("EVACUATE") }
table.insert(ecam, { color = colors.red, text = "RCT DAMAGE CRITICAL", help = "CriticalDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorLost]) then
local items = { white("REACTOR OFF-LINE"), blue("CHECK PLC") }
table.insert(ecam, { color = colors.red, text = "REACTOR CONN LOST", help = "ReactorLost", items = items })
end
if tripped(unit.alarms[ALARM.ReactorDamage]) then
local items = { white("REACTOR DAMAGED"), blue("CHECK RCS"), blue("AWAIT DMG REDUCED") }
table.insert(ecam, { color = colors.red, text = "REACTOR DAMAGE", help = "ReactorDamage", items = items })
end
if tripped(unit.alarms[ALARM.ReactorOverTemp]) then
local items = { white("DAMAGING TEMP"), blue("CHECK RCS"), blue("AWAIT COOLDOWN") }
table.insert(ecam, { color = colors.red, text = "REACTOR OVER TEMP", help = "ReactorOverTemp", items = items })
end
if tripped(unit.alarms[ALARM.ReactorHighTemp]) then
local items = { white("OVER EXPECTED TEMP"), blue("CHECK RCS") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR HIGH TEMP", help = "ReactorHighTemp", items = items})
end
if tripped(unit.alarms[ALARM.ReactorWasteLeak]) then
local items = { white("AT WASTE CAPACITY"), blue("CHECK WASTE OUTPUT"), blue("KEEP RCT DISABLED") }
table.insert(ecam, { color = colors.red, text = "REACTOR WASTE LEAK", help = "ReactorWasteLeak", items = items})
end
if tripped(unit.alarms[ALARM.ReactorHighWaste]) then
local items = { blue("CHECK WASTE OUTPUT") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR WASTE HIGH", help = "ReactorHighWaste", items = items})
end
if tripped(unit.alarms[ALARM.RPSTransient]) then
local items = {}
local stat = unit.reactor_data.rps_status
-- for k, _ in pairs(stat) do stat[k] = true end
local function insert(cond, key, text, color) if cond[key] then table.insert(items, { text = text, help = key, color = color }) end end
table.insert(items, white("REACTOR SCRAMMED"))
insert(stat, "high_dmg", "HIGH DAMAGE", colors.red)
insert(stat, "high_temp", "HIGH TEMPERATURE", colors.red)
insert(stat, "low_cool", "CRIT LOW COOLANT")
insert(stat, "ex_waste", "EXCESS WASTE")
insert(stat, "ex_hcool", "EXCESS HEATED COOL")
insert(stat, "no_fuel", "NO FUEL")
insert(stat, "fault", "HARDWARE FAULT")
insert(stat, "timeout", "SUPERVISOR DISCONN")
insert(stat, "manual", "MANUAL SCRAM", colors.white)
insert(stat, "automatic", "AUTOMATIC SCRAM")
insert(stat, "sys_fail", "NOT FORMED", colors.red)
insert(stat, "force_dis", "FORCE DISABLED", colors.red)
table.insert(items, blue("RESOLVE PROBLEM"))
table.insert(items, blue("RESET RPS"))
table.insert(ecam, { color = colors.yellow, text = "RPS TRANSIENT", help = "RPSTransient", items = items})
end
if tripped(unit.alarms[ALARM.RCSTransient]) then
local items = {}
local annunc = unit.annunciator
-- for k, v in pairs(annunc) do
-- if type(v) == "boolean" then annunc[k] = true end
-- if type(v) == "table" then
-- for a, _ in pairs(v) do
-- v[a] = true
-- end
-- end
-- end
local function insert(cond, key, text, color)
if cond == true or (type(cond) == "table" and cond[key]) then table.insert(items, { text = text, help = key, color = color }) end
end
table.insert(items, white("COOLANT PROBLEM"))
insert(annunc, "RCPTrip", "RCP TRIP", colors.red)
insert(annunc, "CoolantLevelLow", "LOW COOLANT")
if unit.num_boilers == 0 then
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
else
if (util.time_ms() - unit.last_rate_change_ms) > const.FLOW_STABILITY_DELAY_MS then
insert(annunc, "RCSFlowLow", "RCS FLOW LOW")
insert(annunc, "BoilRateMismatch", "BOIL RATE MISMATCH")
insert(annunc, "CoolantFeedMismatch", "COOL FEED MISMATCH")
end
if unit.turbine_flow_stable then
insert(annunc, "SteamFeedMismatch", "STM FEED MISMATCH")
end
end
insert(annunc, "MaxWaterReturnFeed", "MAX WTR RTRN FEED")
for k, v in ipairs(annunc.WaterLevelLow) do insert(v, "WaterLevelLow", "BOILER " .. k .. " WTR LOW", colors.red) end
for k, v in ipairs(annunc.HeatingRateLow) do insert(v, "HeatingRateLow", "BOILER " .. k .. " HEAT RATE") end
for k, v in ipairs(annunc.TurbineOverSpeed) do insert(v, "TurbineOverSpeed", "TURBINE " .. k .. " OVERSPD", colors.red) end
for k, v in ipairs(annunc.GeneratorTrip) do insert(v, "GeneratorTrip", "TURBINE " .. k .. " GEN TRIP") end
table.insert(items, blue("CHECK COOLING SYS"))
table.insert(ecam, { color = colors.yellow, text = "RCS TRANSIENT", help = "RCSTransient", items = items})
end
if tripped(unit.alarms[ALARM.TurbineTrip]) then
local items = {}
for k, v in ipairs(unit.annunciator.TurbineTrip) do
if v then table.insert(items, { text = "TURBINE " .. k .. " TRIP", help = "TurbineTrip" }) end
end
table.insert(items, blue("CHECK ENERGY OUT"))
table.insert(ecam, { color = colors.red, text = "TURBINE TRIP", help = "TurbineTripAlarm", items = items})
end
if not (tripped(unit.alarms[ALARM.ReactorLost]) or unit.connected) then
local items = { blue("CHECK PLC") }
table.insert(ecam, { color = colors.yellow, text = "REACTOR OFF-LINE", items = items })
end
for k, v in ipairs(unit.annunciator.BoilerOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "BOILER " .. k .. " OFF-LINE", items = items})
end
end
for k, v in ipairs(unit.annunciator.TurbineOnline) do
if not v then
local items = { blue("CHECK RTU") }
table.insert(ecam, { color = colors.yellow, text = "TURBINE " .. k .. " OFF-LINE", items = items})
end
end
-- if no alarms, put some basic status messages in
if #ecam == 0 then
table.insert(ecam, { color = colors.green, text = "REACTOR " .. util.trinary(unit.reactor_data.mek_status.status, "NOMINAL", "IDLE"), items = {}})
local plural = util.trinary(unit.num_turbines > 1, "S", "")
table.insert(ecam, { color = colors.green, text = "TURBINE" .. plural .. util.trinary(unit.turbine_flow_stable, " STABLE", " STABILIZING"), items = {}})
end
unit.unit_ps.publish("U_ECAM", textutils.serialize(ecam))
--#endregion
end
-- update control app with unit data from API_GET_CTRL
---@param data table
function iorx.record_control_data(data)
for u_id = 1, #data do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.connected = u_data[1]
unit.reactor_data.rps_tripped = u_data[2]
unit.unit_ps.publish("rps_tripped", u_data[2])
unit.reactor_data.mek_status.status = u_data[3]
unit.unit_ps.publish("status", u_data[3])
unit.reactor_data.mek_status.temp = u_data[4]
unit.unit_ps.publish("temp", u_data[4])
unit.reactor_data.mek_status.burn_rate = u_data[5]
unit.unit_ps.publish("burn_rate", u_data[5])
unit.reactor_data.mek_status.act_burn_rate = u_data[6]
unit.unit_ps.publish("act_burn_rate", u_data[6])
unit.reactor_data.mek_struct.max_burn = u_data[7]
unit.unit_ps.publish("max_burn", u_data[7])
unit.annunciator.AutoControl = u_data[8]
unit.unit_ps.publish("AutoControl", u_data[8])
unit.a_group = u_data[9]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then
control_status = 2
end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
end
end
-- update process app with unit data from API_GET_PROC
---@param data table
function iorx.record_process_data(data)
-- get unit data
for u_id = 1, #io.units do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.reactor_data.mek_status.status = u_data[1]
unit.reactor_data.mek_struct.max_burn = u_data[2]
unit.annunciator.AutoControl = u_data[6]
unit.a_group = u_data[7]
unit.unit_ps.publish("status", u_data[1])
unit.unit_ps.publish("max_burn", u_data[2])
unit.unit_ps.publish("burn_limit", u_data[3])
unit.unit_ps.publish("U_AutoReady", u_data[4])
unit.unit_ps.publish("U_AutoDegraded", u_data[5])
unit.unit_ps.publish("AutoControl", u_data[6])
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
end
-- get facility data
local fac = io.facility
local f_data = data[#io.units + 1]
fac.status_lines = f_data[1]
fac.auto_ready = f_data[2][1]
fac.auto_active = f_data[2][2]
fac.auto_ramping = f_data[2][3]
fac.auto_saturated = f_data[2][4]
fac.auto_scram = f_data[3]
fac.ascram_status = f_data[4]
fac.ps.publish("status_line_1", fac.status_lines[1])
fac.ps.publish("status_line_2", fac.status_lines[2])
fac.ps.publish("auto_ready", fac.auto_ready)
fac.ps.publish("auto_active", fac.auto_active)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_saturated", fac.auto_saturated)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
fac.ps.publish("process_mode", f_data[5][1])
fac.ps.publish("process_burn_target", f_data[5][2])
fac.ps.publish("process_charge_target", f_data[5][3])
fac.ps.publish("process_gen_target", f_data[5][4])
end
-- update waste app with unit data from API_GET_WASTE
---@param data table
function iorx.record_waste_data(data)
-- get unit data
for u_id = 1, #io.units do
local unit = io.units[u_id]
local u_data = data[u_id]
unit.waste_mode = u_data[1]
unit.waste_product = u_data[2]
unit.num_snas = u_data[3]
unit.sna_peak_rate = u_data[4]
unit.sna_max_rate = u_data[5]
unit.sna_out_rate = u_data[6]
unit.waste_stats = u_data[7]
unit.unit_ps.publish("U_AutoWaste", unit.waste_mode == types.WASTE_MODE.AUTO)
unit.unit_ps.publish("U_WasteMode", unit.waste_mode)
unit.unit_ps.publish("U_WasteProduct", unit.waste_product)
unit.unit_ps.publish("sna_count", unit.num_snas)
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)
unit.unit_ps.publish("pu_rate", unit.waste_stats[1])
unit.unit_ps.publish("po_rate", unit.waste_stats[2])
unit.unit_ps.publish("po_pl_rate", unit.waste_stats[3])
end
-- get facility data
local fac = io.facility
local f_data = data[#io.units + 1]
fac.auto_current_waste_product = f_data[1]
fac.auto_pu_fallback_active = f_data[2]
fac.auto_sps_disabled = f_data[3]
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
fac.ps.publish("process_waste_product", f_data[4])
fac.ps.publish("process_pu_fallback", f_data[5])
fac.ps.publish("process_sps_low_power", f_data[6])
fac.waste_stats = f_data[7]
fac.ps.publish("burn_sum", fac.waste_stats[1])
fac.ps.publish("pu_rate", fac.waste_stats[2])
fac.ps.publish("po_rate", fac.waste_stats[3])
fac.ps.publish("po_pl_rate", fac.waste_stats[4])
fac.ps.publish("po_am_rate", fac.waste_stats[5])
fac.ps.publish("spent_waste_rate", fac.waste_stats[6])
fac.sps_ps_tbl[1].publish("SPSStateStatus", f_data[8])
fac.ps.publish("sps_process_rate", f_data[9])
end
-- update facility app with facility and unit data from API_GET_FAC_DTL
---@param data table
function iorx.record_fac_detail_data(data)
local fac = io.facility
local tank_statuses = data[5]
local next_t_stat = 1
-- annunciator
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.auto_scram = data[3]
fac.ascram_status = data[4]
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
fac.ps.publish("rtu_count", fac.rtu_count)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
-- unit data
local units = data[12]
for i = 1, io.facility.num_units do
local unit = io.units[i]
local u_rx = units[i]
unit.connected = u_rx[1]
unit.annunciator = u_rx[2]
unit.reactor_data = u_rx[3]
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then control_status = 2 end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.tank_data_tbl = u_rx[4]
for id = 1, #unit.tank_data_tbl do
local tank = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
end
-- facility dynamic tank data
fac.tank_data_tbl = data[6]
for id = 1, #fac.tank_data_tbl do
local tank = fac.tank_data_tbl[id]
local ps = fac.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
-- induction matrix data
fac.induction_data_tbl[1] = data[8]
local matrix = fac.induction_data_tbl[1]
local m_ps = fac.induction_ps_tbl[1]
local m_stat = data[7]
local mtx_status = 1
if m_stat ~= MTX_STATE.OFFLINE then
if m_stat == MTX_STATE.FAULT then
mtx_status = 3
elseif matrix.formed then
mtx_status = 4
else
mtx_status = 2
end
_record_multiblock_status(m_stat == MTX_STATE.FAULT, matrix, m_ps)
end
m_ps.publish("InductionMatrixStatus", mtx_status)
m_ps.publish("InductionMatrixStateStatus", m_stat)
m_ps.publish("eta_string", data[9][1])
m_ps.publish("avg_charge", data[9][2])
m_ps.publish("avg_inflow", data[9][3])
m_ps.publish("avg_outflow", data[9][4])
m_ps.publish("is_charging", data[9][5])
m_ps.publish("is_discharging", data[9][6])
m_ps.publish("at_max_io", data[9][7])
-- sps data
fac.sps_data_tbl[1] = data[11]
local sps = fac.sps_data_tbl[1]
local s_ps = fac.sps_ps_tbl[1]
local s_stat = data[10]
local sps_status = 1
if s_stat ~= SPS_STATE.OFFLINE then
if s_stat == SPS_STATE.FAULT then
sps_status = 3
elseif sps.formed then
sps_status = 4
else
sps_status = 2
end
_record_multiblock_status(s_stat == SPS_STATE.FAULT, sps, s_ps)
end
s_ps.publish("SPSStatus", sps_status)
s_ps.publish("SPSStateStatus", s_stat)
end
-- update the radiation monitor app with radiation monitor data from API_GET_RAD
---@param data table
function iorx.record_radiation_data(data)
-- unit radiation monitors
for u_id = 1, #io.units do
local unit = io.units[u_id]
local max_rad = 0
local connected = {}
unit.radiation = types.new_zero_radiation_reading()
unit.rad_monitors = data[u_id]
for id, mon in pairs(unit.rad_monitors) do
table.insert(connected, id)
unit.unit_ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
unit.radiation = mon.radiation
end
end
unit.unit_ps.publish("radiation", unit.radiation)
unit.unit_ps.publish("radiation_monitors", textutils.serialize(connected))
end
-- facility radiation monitors
local fac = io.facility
fac.radiation = types.new_zero_radiation_reading()
fac.rad_monitors = data[#io.units + 1]
local max_rad = 0
local connected = {}
for id, mon in pairs(fac.rad_monitors) do
table.insert(connected, id)
fac.ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
fac.radiation = mon.radiation
end
end
fac.ps.publish("radiation", fac.radiation)
fac.ps.publish("radiation_monitors", textutils.serialize(connected))
end
local comp_record = {}
-- update the computers app with the network data from INFO_LIST_CMP
---@param data table
function iorx.record_network_data(data)
local ps = io.ps
local connected = {}
local crd_online = false
ps.publish("comp_online", #data)
-- add/update connected computers
for i = 1, #data do
local entry = data[i]
local type = entry[1]
local id = entry[2]
local pfx = "comp_" .. id
connected[id] = true
if type == DEV_TYPE.SVR then
ps.publish("comp_svr_addr", id)
ps.publish("comp_svr_fw", entry[3])
elseif type == DEV_TYPE.CRD then
crd_online = true
ps.publish("comp_crd_addr", id)
ps.publish("comp_crd_fw", entry[3])
ps.publish("comp_crd_rtt", entry[4])
else
ps.publish(pfx .. "_type", entry[1])
ps.publish(pfx .. "_addr", id)
ps.publish(pfx .. "_fw", entry[3])
ps.publish(pfx .. "_rtt", entry[4])
if type == DEV_TYPE.PLC then
ps.publish(pfx .. "_unit", entry[5])
end
if not comp_record[id] then
comp_record[id] = true
-- trigger the app to create the new element
ps.publish("comp_connect", id)
end
end
end
-- handle the coordinator being online or not
-- no need to worry about the supervisor since this data is from the supervisor, so it has to be 'online' if received
ps.publish("comp_crd_online", crd_online)
if not crd_online then
ps.publish("comp_crd_addr", "---")
ps.publish("comp_crd_fw", "---")
ps.publish("comp_crd_rtt", "---")
end
-- reset the published value
ps.publish("comp_connect", false)
-- remove disconnected computers
for id, state in pairs(comp_record) do
if state and not connected[id] then
comp_record[id] = false
-- trigger the app to delete the element
ps.publish("comp_disconnect", id)
end
end
-- reset the published value
ps.publish("comp_disconnect", false)
end
-- clear the tracked connected computer record
function iorx.clear_comp_record() comp_record = {} end
return function (io_obj)
io = io_obj
return iorx
end

View File

@ -16,14 +16,19 @@ local LINK_STATE = iocontrol.LINK_STATE
local pocket = {}
local MQ__RENDER_CMD = {
UNLOAD_SV_APPS = 1,
UNLOAD_API_APPS = 2
}
local MQ__RENDER_DATA = {
LOAD_APP = 1
}
pocket.MQ__RENDER_CMD = MQ__RENDER_CMD
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
---@type pkt_config
---@diagnostic disable-next-line: missing-fields
local config = {}
pocket.config = config
@ -32,7 +37,6 @@ pocket.config = config
function pocket.load_config()
if not settings.load("/pocket.settings") then return false end
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale")
@ -49,7 +53,6 @@ function pocket.load_config()
local cfv = util.new_validator()
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale)
@ -79,45 +82,40 @@ end
---@enum POCKET_APP_ID
local APP_ID = {
-- core UI
ROOT = 1,
LOADER = 2,
-- main apps
-- main app pages
UNITS = 3,
FACILITY = 4,
CONTROL = 5,
PROCESS = 6,
WASTE = 7,
GUIDE = 8,
ABOUT = 9,
RADMON = 10,
-- diagnostic apps
ALARMS = 11,
COMPS = 12,
-- count
NUM_APPS = 12
CONTROL = 4,
GUIDE = 5,
ABOUT = 6,
-- diag app page
ALARMS = 7,
-- other
DUMMY = 8,
NUM_APPS = 8
}
pocket.APP_ID = APP_ID
---@class nav_tree_page
---@field _p nav_tree_page|nil page's parent
---@field _c nav_tree_page[] page's children
---@field _c table page's children
---@field nav_to function function to navigate to this page
---@field switcher function|nil function to switch between children
---@field tasks function[] tasks to run while viewing this page
---@field tasks table tasks to run while viewing this page
-- initialize the page navigation system
---@param smem pkt_shared_memory
function pocket.init_nav(smem)
local self = {
pane = nil, ---@type AppMultiPane|MultiPane|nil
sidebar = nil, ---@type Sidebar|nil
apps = {}, ---@type pocket_app[]
containers = {}, ---@type Container[]
help_map = {}, ---@type { [string]: function }
help_return = nil, ---@type POCKET_APP_ID|nil
loader_return = nil, ---@type POCKET_APP_ID|nil
pane = nil, ---@type graphics_element
sidebar = nil, ---@type graphics_element
apps = {},
containers = {},
help_map = {},
help_return = nil,
loader_return = nil,
cur_app = APP_ID.ROOT
}
@ -127,27 +125,27 @@ function pocket.init_nav(smem)
local nav = {}
-- set the root pane element to switch between apps with
---@param root_pane MultiPane
---@param root_pane graphics_element
function nav.set_pane(root_pane) self.pane = root_pane end
-- link sidebar element
---@param sidebar Sidebar
---@param sidebar graphics_element
function nav.set_sidebar(sidebar) self.sidebar = sidebar end
-- register an app
---@param app_id POCKET_APP_ID app ID
---@param container Container element that contains this app (usually a Div)
---@param pane? AppMultiPane|MultiPane multipane if this is a simple paned app, then nav_to must be a number
---@param container graphics_element element that contains this app (usually a Div)
---@param pane? graphics_element multipane if this is a simple paned app, then nav_to must be a number
---@param require_sv? boolean true to specifiy if this app should be unloaded when the supervisor connection is lost
---@param require_api? boolean true to specifiy if this app should be unloaded when the api connection is lost
function nav.register_app(app_id, container, pane, require_sv, require_api)
---@class pocket_app
local app = {
loaded = false,
cur_page = nil, ---@type nav_tree_page|nil
cur_page = nil, ---@type nav_tree_page
pane = pane,
paned_pages = {}, ---@type nav_tree_page[]
sidebar_items = {} ---@type sidebar_entry[]
paned_pages = {},
sidebar_items = {}
}
app.load = function () app.loaded = true end
@ -161,27 +159,24 @@ function pocket.init_nav(smem)
function app.requires_conn() return require_sv or require_api or false end
-- delayed set of the pane if it wasn't ready at the start
---@param root_pane AppMultiPane|MultiPane multipane
---@param root_pane graphics_element multipane
function app.set_root_pane(root_pane)
app.pane = root_pane
end
-- configure the sidebar
---@param items sidebar_entry[]
---@param items table
function app.set_sidebar(items)
app.sidebar_items = items
-- only modify the sidebar if this app is still open
if self.cur_app == app_id then
if self.sidebar then self.sidebar.update(items) end
end
if self.sidebar then self.sidebar.update(items) end
end
-- function to run on initial load into memory
---@param on_load function callback
function app.set_load(on_load)
app.load = function ()
app.loaded = true -- must flag first so it can't be repeatedly attempted
on_load()
app.loaded = true
end
end
@ -189,8 +184,8 @@ function pocket.init_nav(smem)
---@param on_unload function callback
function app.set_unload(on_unload)
app.unload = function ()
app.loaded = false
on_unload()
app.loaded = false
end
end
@ -264,28 +259,20 @@ function pocket.init_nav(smem)
-- open an app
---@param app_id POCKET_APP_ID
---@param on_ready? function
function nav.open_app(app_id, on_ready)
function nav.open_app(app_id)
-- reset help return on navigating out of an app
if app_id == APP_ID.ROOT then self.help_return = nil end
local app = self.apps[app_id]
local app = self.apps[app_id] ---@type pocket_app
if app then
local p_comms = smem.pkt_sys.pocket_comms
local req_sv, req_api = app.check_requires()
if (req_sv and not p_comms.is_sv_linked()) or (req_api and not p_comms.is_api_linked()) then
-- report required connction(s)
iocontrol.get_db().loader_require = { sv = req_sv, api = req_api }
iocontrol.get_db().ps.toggle("loader_reqs")
if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then
-- bring up the app loader
self.loader_return = app_id
app_id = APP_ID.LOADER
app = self.apps[app_id]
else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, app_id) end
self.cur_app = app_id
self.pane.set_value(app_id)
@ -293,16 +280,11 @@ function pocket.init_nav(smem)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
if app.loaded and on_ready then on_ready() end
else
log.debug("tried to open unknown app")
end
end
-- go home (open the home screen app)
function nav.go_home() nav.open_app(APP_ID.ROOT) end
-- open the app that was blocked on connecting
function nav.on_loader_connected()
if self.loader_return then
@ -342,7 +324,7 @@ function pocket.init_nav(smem)
function nav.get_containers() return self.containers end
-- get the currently active page
---@return nav_tree_page|nil
---@return nav_tree_page
function nav.get_current_page()
return self.apps[self.cur_app].get_current_page()
end
@ -357,7 +339,7 @@ function pocket.init_nav(smem)
return
end
local app = self.apps[self.cur_app]
local app = self.apps[self.cur_app] ---@type pocket_app
log.debug("attempting app nav up for app " .. self.cur_app)
if not app.nav_up() then
@ -370,13 +352,13 @@ function pocket.init_nav(smem)
function nav.open_help(key)
self.help_return = self.cur_app
nav.open_app(APP_ID.GUIDE, function ()
if self.help_map[key] then self.help_map[key]() end
end)
nav.open_app(APP_ID.GUIDE)
local load = self.help_map[key]
if load then load() end
end
-- link the help map from the guide app
---@param map { [string]: function }
function nav.link_help(map) self.help_map = map end
return nav
@ -559,41 +541,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end
-- supervisor get connected computers
function public.diag__get_computers()
if self.sv.linked then _send_sv(MGMT_TYPE.INFO_LIST_CMP, {}) end
end
-- coordinator get facility app data
function public.api__get_facility()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) end
end
-- coordinator get unit data
function public.api__get_unit(unit)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
end
-- coordinator get control app data
function public.api__get_control()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_CTRL, {}) end
end
-- coordinator get process app data
function public.api__get_process()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_PROC, {}) end
end
-- coordinator get waste app data
function public.api__get_waste()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end
end
-- coordinator get radiation app data
function public.api__get_rad()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_RAD, {}) end
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
@ -601,12 +553,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
_send_api(CRDN_TYPE.FAC_CMD, { cmd, option })
end
-- send the auto process control configuration with a start command
---@param auto_cfg [ PROCESS, number, number, number, number[] ]
function public.send_auto_start(auto_cfg)
_send_api(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.START, table.unpack(auto_cfg) })
end
-- send a unit command
---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
@ -670,7 +616,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet)
local diag = iocontrol.get_db().diag
local ps = iocontrol.get_db().ps
if packet ~= nil then
local l_chan = packet.scada_frame.local_channel()
@ -710,9 +655,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMAND.START then
iocontrol.get_db().facility.start_ack(ack)
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
@ -749,31 +692,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
end
elseif packet.type == CRDN_TYPE.API_GET_FAC then
if _check_length(packet, 11) then
iocontrol.rx.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_FAC_DTL then
if _check_length(packet, 12) then
iocontrol.rx.record_fac_detail_data(packet.data)
iocontrol.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
iocontrol.rx.record_unit_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_CTRL then
if _check_length(packet, #iocontrol.get_db().units) then
iocontrol.rx.record_control_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_PROC then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_process_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_WASTE then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_waste_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_RAD then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_radiation_data(packet.data)
iocontrol.record_unit_data(packet.data)
end
else _fail_type(packet) end
else
@ -922,23 +845,23 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
if _check_length(packet, 8) then
for i = 1, #packet.data do
ps.publish("alarm_tone_" .. i, packet.data[i] == true)
diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
end
end
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then
ps.publish("alarm_ready_warn", "testing denied")
diag.tone_test.ready_warn.set_value("testing denied")
log.debug("supervisor SCADA diag tone set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1]
local states = packet.data[2]
ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then
diag.tone_test.tone_buttons[i].set_value(states[i] == true)
ps.publish("alarm_tone_" .. i, states[i] == true)
diag.tone_test.tone_indicators[i].update(states[i] == true)
end
end
else
@ -946,13 +869,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
end
elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then
if packet.length == 1 and packet.data[1] == false then
ps.publish("alarm_ready_warn", "testing denied")
diag.tone_test.ready_warn.set_value("testing denied")
log.debug("supervisor SCADA diag alarm set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1]
local states = packet.data[2]
ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then
@ -962,8 +885,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end
elseif packet.type == MGMT_TYPE.INFO_LIST_CMP then
iocontrol.rx.record_network_data(packet.data)
else _fail_type(packet) end
elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established

View File

@ -6,8 +6,8 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
local F_CMD = comms.FAC_COMMAND
local U_CMD = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
---@class pocket_process_controller
local process = {}
@ -25,32 +25,23 @@ function process.init(iocontrol, pocket_comms)
self.comms = pocket_comms
end
------------------------------
--#region FACILITY COMMANDS --
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(F_CMD.SCRAM_ALL)
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
--#endregion
------------------------------
--------------------------
--#region UNIT COMMANDS --
-- start reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(U_CMD.START, id)
self.comms.send_unit_command(UNIT_COMMAND.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
@ -58,14 +49,14 @@ end
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(U_CMD.SCRAM, id)
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(U_CMD.RESET_RPS, id)
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
@ -73,30 +64,14 @@ end
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- assign a unit to a group
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
end
-- set waste mode
---@param id integer unit ID
---@param mode integer waste mode
function process.set_unit_waste(id, mode)
self.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
@ -104,7 +79,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
@ -112,55 +87,8 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
-- #endregion
--------------------------
---------------------------------
--#region AUTO PROCESS CONTROL --
-- process start command
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function process.process_start(mode, burn_target, charge_target, gen_target, limits)
self.comms.send_auto_start({ mode, burn_target, charge_target, gen_target, limits })
log.debug("PROCESS: START AUTO CTRL")
end
-- process stop command
function process.process_stop()
self.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTRL")
end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
self.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
self.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
self.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
end
-- #endregion
---------------------------------
return process

View File

@ -8,7 +8,7 @@ local style = require("pocket.ui.style")
local core = require("graphics.core")
local flasher = require("graphics.flasher")
local DisplayBox = require("graphics.elements.DisplayBox")
local DisplayBox = require("graphics.elements.displaybox")
---@class pocket_renderer
local renderer = {}

View File

@ -2,10 +2,8 @@
-- SCADA System Access on a Pocket Computer
--
---@diagnostic disable-next-line: lowercase-global
pocket = pocket or periphemu -- luacheck: ignore pocket
local _is_pocket_env = pocket -- luacheck: ignore pocket
---@diagnostic disable-next-line: undefined-global
local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket
require("/initenv").init_env()
@ -22,7 +20,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v1.0.3"
local POCKET_VERSION = "v0.12.1-alpha"
local println = util.println
local println_ts = util.println_ts

View File

@ -14,6 +14,7 @@ local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = pocket.MQ__RENDER_CMD
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA
-- main thread
@ -57,10 +58,8 @@ function threads.thread__main(smem)
pocket_comms.link_update()
-- update any tasks for the active page
if nav.get_current_page() then
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
end
local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end
loop_clock.start()
elseif sv_wd.is_timer(param1) then
@ -158,23 +157,23 @@ function threads.thread__render(smem)
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
if msg.message == MQ__RENDER_CMD.UNLOAD_SV_APPS then
elseif msg.message == MQ__RENDER_CMD.UNLOAD_API_APPS then
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
local cmd = msg.message ---@type queue_data
if cmd.key == MQ__RENDER_DATA.LOAD_APP then
log.debug("RENDER: load app " .. cmd.val[1])
log.debug("RENDER: load app " .. cmd.val)
local draw_start = util.time_ms()
pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val[1]) end)
pkt_state.ui_ok, pkt_state.ui_error = pcall(function () nav.load_app(cmd.val) end)
if not pkt_state.ui_ok then
log.fatal(util.c("RENDER: app load failed with error ", pkt_state.ui_error))
else
log.debug("RENDER: app loaded in " .. (util.time_ms() - draw_start) .. "ms")
-- call the on loaded function if provided
if type(cmd.val[2]) == "function" then cmd.val[2]() end
end
end
elseif msg.qtype == mqueue.TYPE.PACKET then

View File

@ -1,184 +0,0 @@
--
-- Alarm Test App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local SwitchButton = require("graphics.elements.controls.SwitchButton")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
-- create alarm test page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local ps = db.ps
local ttest = db.diag.tone_test
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.ALARMS, frame, nil, true)
local main = Div{parent=frame,x=1,y=1}
local page_div = Div{parent=main,y=2,width=main.get_width()}
--#region alarm testing
local alarm_page = app.new_page(nil, 1)
alarm_page.tasks = { db.diag.tone_test.get_tone_states }
local alarms_div = Div{parent=page_div}
TextBox{parent=alarms_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
local alarm_ready_warn = TextBox{parent=alarms_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
alarm_ready_warn.register(ps, "alarm_ready_warn", alarm_ready_warn.set_value)
local alarm_page_states = Div{parent=alarms_div,x=2,y=3,height=5,width=8}
TextBox{parent=alarm_page_states,text="States",alignment=ALIGN.CENTER}
local ta_1 = IndicatorLight{parent=alarm_page_states,label="1",colors=c_blue_gray}
local ta_2 = IndicatorLight{parent=alarm_page_states,label="2",colors=c_blue_gray}
local ta_3 = IndicatorLight{parent=alarm_page_states,label="3",colors=c_blue_gray}
local ta_4 = IndicatorLight{parent=alarm_page_states,label="4",colors=c_blue_gray}
local ta_5 = IndicatorLight{parent=alarm_page_states,x=6,y=2,label="5",colors=c_blue_gray}
local ta_6 = IndicatorLight{parent=alarm_page_states,x=6,label="6",colors=c_blue_gray}
local ta_7 = IndicatorLight{parent=alarm_page_states,x=6,label="7",colors=c_blue_gray}
local ta_8 = IndicatorLight{parent=alarm_page_states,x=6,label="8",colors=c_blue_gray}
local ta = { ta_1, ta_2, ta_3, ta_4, ta_5, ta_6, ta_7, ta_8 }
for i = 1, #ta do
ta[i].register(ps, "alarm_tone_" .. i, ta[i].update)
end
local alarms = Div{parent=alarms_div,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=alarms_div.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
--#endregion
--#region direct tone testing
local tones_page = app.new_page(nil, 2)
tones_page.tasks = { db.diag.tone_test.get_tone_states }
local tones_div = Div{parent=page_div}
TextBox{parent=tones_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
local tone_ready_warn = TextBox{parent=tones_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
tone_ready_warn.register(ps, "alarm_ready_warn", tone_ready_warn.set_value)
local tone_page_states = Div{parent=tones_div,x=3,y=3,height=5,width=8}
TextBox{parent=tone_page_states,text="States",alignment=ALIGN.CENTER}
local tt_1 = IndicatorLight{parent=tone_page_states,label="1",colors=c_blue_gray}
local tt_2 = IndicatorLight{parent=tone_page_states,label="2",colors=c_blue_gray}
local tt_3 = IndicatorLight{parent=tone_page_states,label="3",colors=c_blue_gray}
local tt_4 = IndicatorLight{parent=tone_page_states,label="4",colors=c_blue_gray}
local tt_5 = IndicatorLight{parent=tone_page_states,x=6,y=2,label="5",colors=c_blue_gray}
local tt_6 = IndicatorLight{parent=tone_page_states,x=6,label="6",colors=c_blue_gray}
local tt_7 = IndicatorLight{parent=tone_page_states,x=6,label="7",colors=c_blue_gray}
local tt_8 = IndicatorLight{parent=tone_page_states,x=6,label="8",colors=c_blue_gray}
local tt = { tt_1, tt_2, tt_3, tt_4, tt_5, tt_6, tt_7, tt_8 }
for i = 1, #tt do
tt[i].register(ps, "alarm_tone_" .. i, tt[i].update)
end
local tones = Div{parent=tones_div,x=14,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=tones_div.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
--#endregion
--#region info page
app.new_page(nil, 3)
local info_div = Div{parent=page_div}
TextBox{parent=info_div,x=2,y=1,text="This app provides tools to test alarm sounds by alarm and by tone (1-8)."}
TextBox{parent=info_div,x=2,y=6,text="The system must be idle (all units stopped with no alarms active) for testing to run."}
TextBox{parent=info_div,x=2,y=12,text="Currently, testing will be denied unless you have a Facility Authentication Key set (this will change in the future)."}
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes={alarms_div,tones_div,info_div}}
app.set_root_pane(u_pane)
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = function () app.switcher(1) end },
{ label = " \x0f ", color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(2) end },
{ label = " ? ", color = core.cpair(colors.black, colors.blue), callback = function () app.switcher(3) end }
}
app.set_sidebar(list)
end
return new_view

View File

@ -1,297 +0,0 @@
--
-- Computer List App
--
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local DEV_TYPE = comms.DEVICE_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local lu_col = style.label_unit_pair
local box_label = cpair(colors.lightGray, colors.gray)
-- new computer list page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.COMPS, frame, nil, true, false)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local ps = db.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- create all page divs
for _ = 1, 4 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 1s it will re-send the query
local function update()
if util.time_ms() - last_update >= 1000 then
db.diag.get_comps()
last_update = util.time_ms()
end
end
-- create indicators for the ID, firmware, and RTT
---@param pfx string
---@param rect Rectangle
local function create_common_indicators(pfx, rect)
local first = TextBox{parent=rect,text="Computer",fg_bg=box_label}
TextBox{parent=rect,text="Firmware",fg_bg=box_label}
TextBox{parent=rect,text="RTT (ms)",fg_bg=box_label}
local y = first.get_y()
local addr = TextBox{parent=rect,x=10,y=y,text="---"}
local fw = TextBox{parent=rect,x=10,y=y+1,text="---"}
local rtt = TextBox{parent=rect,x=10,y=y+2,text="---"}
addr.register(ps, pfx .. "_addr", function (v) addr.set_value(util.strval(v)) end)
fw.register(ps, pfx .. "_fw", function (v) fw.set_value(util.strval(v)) end)
rtt.register(ps, pfx .. "_rtt", function (value)
rtt.set_value(util.strval(value))
if value == "---" then
rtt.recolor(colors.white)
elseif value > const.HIGH_RTT then
rtt.recolor(colors.red)
elseif value > const.WARN_RTT then
rtt.recolor(colors.yellow)
else
rtt.recolor(colors.green)
end
end)
end
--#region main computer page
local m_div = Div{parent=panes[1],x=2,width=main.get_width()-2}
local main_page = app.new_page(nil, 1)
main_page.tasks = { update }
TextBox{parent=m_div,y=1,text="Connected Computers",alignment=ALIGN.CENTER}
local conns = DataIndicator{parent=m_div,y=3,lu_colors=lu_col,label="Total Online",unit="",format="%8d",value=0,commas=true,width=21}
conns.register(ps, "comp_online", conns.update)
local svr_div = Div{parent=m_div,y=4,height=6}
local svr_rect = Rectangle{parent=svr_div,height=6,width=22,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=svr_rect,text="Supervisor"}
TextBox{parent=svr_rect,text="Status",fg_bg=box_label}
TextBox{parent=svr_rect,x=10,y=2,text="Online",fg_bg=cpair(colors.green,colors._INHERIT)}
TextBox{parent=svr_rect,text="Computer",fg_bg=box_label}
TextBox{parent=svr_rect,text="Firmware",fg_bg=box_label}
local svr_addr = TextBox{parent=svr_rect,x=10,y=3,text="?"}
local svr_fw = TextBox{parent=svr_rect,x=10,y=4,text="?"}
svr_addr.register(ps, "comp_svr_addr", function (v) svr_addr.set_value(util.strval(v)) end)
svr_fw.register(ps, "comp_svr_fw", function (v) svr_fw.set_value(util.strval(v)) end)
local crd_div = Div{parent=m_div,y=11,height=7}
local crd_rect = Rectangle{parent=crd_div,height=7,width=21,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=crd_rect,text="Coordinator"}
TextBox{parent=crd_rect,text="Status",fg_bg=box_label}
local crd_online = TextBox{parent=crd_rect,x=10,y=2,width=8,text="Off-line",fg_bg=cpair(colors.red,colors._INHERIT)}
create_common_indicators("comp_crd", crd_rect)
crd_online.register(ps, "comp_crd_online", function (online)
if online then
crd_online.recolor(colors.green)
crd_online.set_value("Online")
else
crd_online.recolor(colors.red)
crd_online.set_value("Off-line")
end
end)
--#endregion
--#region PLC page
local p_div = Div{parent=panes[2],width=main.get_width()}
local plc_page = app.new_page(nil, 2)
plc_page.tasks = { update }
TextBox{parent=p_div,y=1,text="PLC Devices",alignment=ALIGN.CENTER}
local plc_list = ListBox{parent=p_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local plc_elems = {} ---@type graphics_element[]
--#endregion
--#region RTU gateway page
local r_div = Div{parent=panes[3],width=main.get_width()}
local rtu_page = app.new_page(nil, 3)
rtu_page.tasks = { update }
TextBox{parent=r_div,y=1,text="RTU Gateway Devices",alignment=ALIGN.CENTER}
local rtu_list = ListBox{parent=r_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local rtu_elems = {} ---@type graphics_element[]
--#endregion
--#region pocket computer page
local pk_div = Div{parent=panes[4],width=main.get_width()}
local pkt_page = app.new_page(nil, 4)
pkt_page.tasks = { update }
TextBox{parent=pk_div,y=1,text="Pocket Devices",alignment=ALIGN.CENTER}
local pkt_list = ListBox{parent=pk_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local pkt_elems = {} ---@type graphics_element[]
--#endregion
--#region connect/disconnect management
ps.subscribe("comp_connect", function (id)
if id == false then return end
local pfx = "comp_" .. id
local type = ps.get(pfx .. "_type")
if type == DEV_TYPE.PLC then
plc_elems[id] = Div{parent=plc_list,height=7}
local rect = Rectangle{parent=plc_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
local title = TextBox{parent=rect,text="PLC (Unit ?)"}
title.register(ps, pfx .. "_unit", function (unit) title.set_value("PLC (Unit " .. unit .. ")") end)
create_common_indicators(pfx, rect)
elseif type == DEV_TYPE.RTU then
rtu_elems[id] = Div{parent=rtu_list,height=7}
local rect = Rectangle{parent=rtu_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=rect,text="RTU Gateway"}
create_common_indicators(pfx, rect)
elseif type == DEV_TYPE.PKT then
pkt_elems[id] = Div{parent=pkt_list,height=7}
local rect = Rectangle{parent=pkt_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=rect,text="Pocket Computer"}
create_common_indicators(pfx, rect)
end
end)
ps.subscribe("comp_disconnect", function (id)
if id == false then return end
local type = ps.get("comp_" ..id .. "_type")
if type == DEV_TYPE.PLC then
if plc_elems[id] then plc_elems[id].delete() end
plc_elems[id] = nil
elseif type == DEV_TYPE.RTU then
if rtu_elems[id] then rtu_elems[id].delete() end
rtu_elems[id] = nil
elseif type == DEV_TYPE.PKT then
if pkt_elems[id] then pkt_elems[id].delete() end
pkt_elems[id] = nil
end
end)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " @ ", color = core.cpair(colors.black, colors.blue), callback = main_page.nav_to },
{ label = "PLC", color = core.cpair(colors.black, colors.red), callback = plc_page.nav_to },
{ label = "RTU", color = core.cpair(colors.black, colors.orange), callback = rtu_page.nav_to },
{ label = "PKT", color = core.cpair(colors.black, colors.lightGray), callback = pkt_page.nav_to }
}
app.set_sidebar(list)
-- done, show the app
main_page.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
-- clear the list of connected computers so that connections re-appear on reload of this app
iocontrol.rx.clear_comp_record()
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -1,5 +1,5 @@
--
-- Facility & Unit Control App
-- Unit Control Page
--
local types = require("scada-common.types")
@ -13,19 +13,19 @@ local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local WaitingAnim = require("graphics.elements.animations.waiting")
local HazardButton = require("graphics.elements.controls.HazardButton")
local PushButton = require("graphics.elements.controls.PushButton")
local HazardButton = require("graphics.elements.controls.hazard_button")
local PushButton = require("graphics.elements.controls.push_button")
local NumberField = require("graphics.elements.form.NumberField")
local NumberField = require("graphics.elements.form.number_field")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
local AUTO_GROUP = types.AUTO_GROUP
@ -34,21 +34,16 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local mode_states = style.icon_states.mode_states
local mode_states = style.icon_states.mode_states
local btn_active = cpair(colors.white, colors.black)
local hzd_fg_bg = style.hzd_fg_bg
local hzd_dis_colors = style.hzd_dis_colors
local hzd_fg_bg = cpair(colors.white, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray)
-- new unit control page view
---@param root Container parent
---@param root graphics_element parent
local function new_view(root)
local btn_fg_bg = cpair(colors.green, colors.black)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
@ -63,14 +58,17 @@ local function new_view(root)
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
local page_div = nil ---@type Div|nil
local btn_fg_bg = cpair(colors.green, colors.black)
local btn_active = cpair(colors.white, colors.black)
local page_div = nil ---@type nil|graphics_element
-- set sidebar to display unit-specific fields based on a specified unit
local function set_sidebar()
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end }
}
@ -85,7 +83,7 @@ local function new_view(root)
local function load()
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
local panes = {}
local active_unit = 1
@ -107,21 +105,21 @@ local function new_view(root)
app.switcher(active_unit)
end
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_ctrl()
last_update = util.time_ms()
end
end
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i]
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- refresh data callback, every 500ms it will re-send the query
local last_update = 0
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_unit(i)
last_update = util.time_ms()
end
end
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
@ -140,12 +138,12 @@ local function new_view(root)
u_div.line_break()
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=label_fg_bg}
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=label_fg_bg}
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=style.field,dis_fg_bg=style.field_disable}
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.lightGray)}
local set_burn = function () unit.set_burn(burn_cmd.get_numeric()) end
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=style.btn_disable,callback=set_burn}
local set_burn = function () unit.set_burn(burn_cmd.get_value()) end
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=cpair(colors.gray,colors.black),callback=set_burn}
-- enable/disable controls based on group assignment (start button is separate)
burn_cmd.register(u_ps, "auto_group_id", function (gid)
@ -158,10 +156,10 @@ local function new_view(root)
burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value)
burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max)
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg,dis_colors=hzd_dis_colors}
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg}
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg}
unit.start_ack = start.on_response
unit.ack_alarms_ack = ack_a.on_response
@ -169,10 +167,12 @@ local function new_view(root)
unit.reset_rps_ack = reset.on_response
local function start_button_en_check()
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
end
start.register(u_ps, "status", start_button_en_check)
@ -194,8 +194,8 @@ local function new_view(root)
TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER}
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=hzd_dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=hzd_dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
db.facility.scram_ack = scram.on_response
db.facility.ack_alarms_ack = ack_a.on_response
@ -217,7 +217,7 @@ local function new_view(root)
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
app.delete_pages()
-- show loading screen

View File

@ -0,0 +1,118 @@
--
-- Diagnostic Apps
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local IndicatorLight = require("graphics.elements.indicators.light")
local Checkbox = require("graphics.elements.controls.checkbox")
local PushButton = require("graphics.elements.controls.push_button")
local SwitchButton = require("graphics.elements.controls.switch_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create diagnostic app pages
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
------------------------
-- Alarm Testing Page --
------------------------
local alarm_test = Div{parent=root,x=1,y=1}
local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test, nil, true)
local page = alarm_app.new_page(nil, function () end)
page.tasks = { db.diag.tone_test.get_tone_states }
local ttest = db.diag.tone_test
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",alignment=ALIGN.CENTER}
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}
local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray}
local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray}
local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray}
local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray}
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
end
return create_pages

View File

@ -0,0 +1,29 @@
--
-- Placeholder App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local APP_ID = pocket.APP_ID
-- create placeholder app page
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(APP_ID.DUMMY, main).new_page(nil, function () end)
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)}
end
return create_pages

View File

@ -1,258 +0,0 @@
--
-- Facility Overview App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local dyn_tank = require("pocket.ui.pages.dynamic_tank")
local facility_sps = require("pocket.ui.pages.facility_sps")
local induction_mtx = require("pocket.ui.pages.facility_matrix")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
local basic_states = style.icon_states.basic_states
local mode_states = style.icon_states.mode_states
local red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
local grn_ind_s = style.icon_states.grn_ind_s
-- new unit page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.FACILITY, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local tank_page_navs = {}
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local fac = db.facility
local f_ps = fac.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- refresh data callback, every 500ms it will re-send the query
local last_update = 0
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_fac()
last_update = util.time_ms()
end
end
--#region facility overview
local main_pane = Div{parent=page_div}
local f_div = Div{parent=main_pane,x=2,width=main.get_width()-2}
table.insert(panes, main_pane)
local fac_page = app.new_page(nil, #panes)
fac_page.tasks = { update }
TextBox{parent=f_div,y=1,text="Facility",alignment=ALIGN.CENTER}
local mtx_state = IconIndicator{parent=f_div,y=3,label="Matrix Status",states=basic_states}
local sps_state = IconIndicator{parent=f_div,label="SPS Status",states=basic_states}
mtx_state.register(fac.induction_ps_tbl[1], "InductionMatrixStatus", mtx_state.update)
sps_state.register(fac.sps_ps_tbl[1], "SPSStatus", sps_state.update)
TextBox{parent=f_div,y=6,text="RTU Gateways",fg_bg=label_fg_bg}
local rtu_count = DataIndicator{parent=f_div,x=19,y=6,label="",format="%3d",value=0,lu_colors=lu_col,width=3}
rtu_count.register(f_ps, "rtu_count", rtu_count.update)
TextBox{parent=f_div,y=8,text="Induction Matrix",alignment=ALIGN.CENTER}
local eta = TextBox{parent=f_div,x=1,y=10,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,colors.gray)}
eta.register(fac.induction_ps_tbl[1], "eta_string", eta.set_value)
TextBox{parent=f_div,y=12,text="Unit Statuses",alignment=ALIGN.CENTER}
f_div.line_break()
for i = 1, fac.num_units do
local ctrl = IconIndicator{parent=f_div,label="U"..i.." Control State",states=mode_states}
ctrl.register(db.units[i].unit_ps, "U_ControlStatus", ctrl.update)
end
--#endregion
--#region facility annunciator
local a_pane = Div{parent=page_div}
local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}
table.insert(panes, a_pane)
local annunc_page = app.new_page(nil, #panes)
annunc_page.tasks = { update }
TextBox{parent=a_div,y=1,text="Annunciator",alignment=ALIGN.CENTER}
local all_ok = IconIndicator{parent=a_div,y=3,label="Units Online",states=grn_ind_s}
local ind_mat = IconIndicator{parent=a_div,label="Induction Matrix",states=grn_ind_s}
local sps = IconIndicator{parent=a_div,label="SPS Connected",states=grn_ind_s}
all_ok.register(f_ps, "all_sys_ok", all_ok.update)
ind_mat.register(fac.induction_ps_tbl[1], "InductionMatrixStateStatus", function (status) ind_mat.update(status > 1) end)
sps.register(fac.sps_ps_tbl[1], "SPSStateStatus", function (status) sps.update(status > 1) end)
a_div.line_break()
local auto_scram = IconIndicator{parent=a_div,label="Automatic SCRAM",states=red_ind_s}
local matrix_flt = IconIndicator{parent=a_div,label="Ind. Matrix Fault",states=yel_ind_s}
local matrix_fill = IconIndicator{parent=a_div,label="Matrix Charge Hi",states=red_ind_s}
local unit_crit = IconIndicator{parent=a_div,label="Unit Crit. Alarm",states=red_ind_s}
local fac_rad_h = IconIndicator{parent=a_div,label="FAC Radiation Hi",states=red_ind_s}
local gen_fault = IconIndicator{parent=a_div,label="Gen Control Fault",states=yel_ind_s}
auto_scram.register(f_ps, "auto_scram", auto_scram.update)
matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)
gen_fault.register(f_ps, "as_gen_fault", gen_fault.update)
--#endregion
--#region induction matrix
local mtx_page_nav = induction_mtx(app, panes, Div{parent=page_div}, fac.induction_ps_tbl[1], update)
--#endregion
--#region SPS
local sps_page_nav = facility_sps(app, panes, Div{parent=page_div}, fac.sps_ps_tbl[1], update)
--#endregion
--#region facility tank pages
local t_pane = Div{parent=page_div}
local t_div = Div{parent=t_pane,x=2,width=main.get_width()-2}
table.insert(panes, t_pane)
local tank_page = app.new_page(nil, #panes)
tank_page.tasks = { update }
TextBox{parent=t_div,y=1,text="Facility Tanks",alignment=ALIGN.CENTER}
local f_tank_id = 1
for t = 1, #fac.tank_list do
if fac.tank_list[t] == 1 then
t_div.line_break()
local tank = IconIndicator{parent=t_div,x=1,label="Unit Tank "..t.." (U-"..t..")",states=basic_states}
tank.register(db.units[t].tank_ps_tbl[1], "DynamicTankStatus", tank.update)
TextBox{parent=t_div,x=5,text="\x07 Unit "..t,fg_bg=label_fg_bg}
elseif fac.tank_list[t] == 2 then
tank_page_navs[f_tank_id] = dyn_tank(app, nil, panes, Div{parent=page_div}, t, fac.tank_ps_tbl[f_tank_id], update)
t_div.line_break()
local tank = IconIndicator{parent=t_div,x=1,label="Fac. Tank "..f_tank_id.." (F-"..f_tank_id..")",states=basic_states}
tank.register(fac.tank_ps_tbl[f_tank_id], "DynamicTankStatus", tank.update)
local connections = ""
for i = 1, #fac.tank_conns do
if fac.tank_conns[i] == t then
if connections ~= "" then
connections = connections .. "\n\x07 Unit " .. i
else
connections = "\x07 Unit " .. i
end
end
end
TextBox{parent=t_div,x=5,text=connections,fg_bg=label_fg_bg}
f_tank_id = f_tank_id + 1
end
end
--#endregion
-- setup multipane
local f_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(f_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = "FAC", tall = true, color = core.cpair(colors.black, colors.orange), callback = fac_page.nav_to },
{ label = "ANN", color = core.cpair(colors.black, colors.yellow), callback = annunc_page.nav_to },
{ label = "MTX", color = core.cpair(colors.black, colors.white), callback = mtx_page_nav },
{ label = "SPS", color = core.cpair(colors.black, colors.purple), callback = sps_page_nav },
{ label = "TNK", tall = true, color = core.cpair(colors.black, colors.blue), callback = tank_page.nav_to }
}
for i = 1, #fac.tank_data_tbl do
table.insert(list, { label = "F-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = tank_page_navs[i] })
end
app.set_sidebar(list)
-- done, show the app
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -9,29 +9,34 @@ local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local docs = require("pocket.ui.docs")
-- local style = require("pocket.ui.style")
local guide_section = require("pocket.ui.pages.guide_section")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local WaitingAnim = require("graphics.elements.animations.waiting")
local PushButton = require("graphics.elements.controls.PushButton")
local PushButton = require("graphics.elements.controls.push_button")
local TextField = require("graphics.elements.form.TextField")
local TextField = require("graphics.elements.form.text_field")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- local label = style.label
-- local lu_col = style.label_unit_pair
-- local text_fg = style.text_fg
-- new system guide view
---@param root Container parent
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
@ -42,30 +47,23 @@ local function new_view(root)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
local load_text_1 = TextBox{parent=load_div,y=14,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
local load_text_2 = TextBox{parent=load_div,y=15,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
-- give more detailed information so the user doesn't give up
local function load_text(a, b)
if a then load_text_1.set_value(a) end
load_text_2.set_value(b or "")
end
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
local btn_fg_bg = cpair(colors.cyan, colors.black)
local btn_active = cpair(colors.white, colors.black)
local btn_disable = cpair(colors.gray, colors.black)
app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }})
app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end }})
local page_div = nil ---@type Div|nil
local page_div = nil ---@type nil|graphics_element
-- load the app (create the elements)
local function load()
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = " \x14 ", color = core.cpair(colors.black, colors.cyan), callback = function () app.switcher(1) end },
{ label = "__?", color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(2) end }
}
@ -73,7 +71,7 @@ local function new_view(root)
app.set_sidebar(list)
page_div = Div{parent=main,y=2}
local p_width = page_div.get_width() - 1
local p_width = page_div.get_width() - 2
local main_page = app.new_page(nil, 1)
local search_page = app.new_page(main_page, 2)
@ -90,11 +88,12 @@ local function new_view(root)
local fps = Div{parent=page_div,x=2,width=p_width}
local gls = Div{parent=page_div,x=2,width=p_width}
local lnk = Div{parent=page_div,x=2,width=p_width}
local panes = { home, search, use, uis, fps, gls, lnk } ---@type Div[]
local panes = { home, search, use, uis, fps, gls, lnk }
local doc_map = {} ---@type { [string]: function }
local search_db = {} ---@type [ string, string, string, function ][]
local doc_map = {}
local search_db = {}
---@class _guide_section_constructor_data
local sect_construct_data = { app, page_div, panes, doc_map, search_db, btn_fg_bg, btn_active }
TextBox{parent=home,y=1,text="cc-mek-scada Guide",alignment=ALIGN.CENTER}
@ -106,8 +105,6 @@ local function new_view(root)
PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to}
PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to}
load_text("Search")
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)}
@ -120,7 +117,7 @@ local function new_view(root)
function func_ref.run_search()
local query = string.lower(query_field.get_value())
local s_results = { {}, {}, {}, {} } ---@type [ string, string, string, function ][][]
local s_results = { {}, {}, {}, {} }
search_results.remove_all()
@ -158,7 +155,7 @@ local function new_view(root)
for idx = 1, #s_results[tier] do
local entry = s_results[tier][idx]
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)}
PushButton{parent=search_results,text=entry[2],alignment=ALIGN.LEFT,fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
PushButton{parent=search_results,text=entry[2],fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
empty = false
end
@ -175,29 +172,14 @@ local function new_view(root)
util.nop()
load_text("System Usage")
TextBox{parent=use,y=1,text="System Usage",alignment=ALIGN.CENTER}
PushButton{parent=use,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
load_text(false, "Connecting Devices")
local conn_dev_page = guide_section(sect_construct_data, use_page, "Connecting Devs", docs.usage.conn, 110)
load_text(false, "Configuring Devices")
local config_dev_page = guide_section(sect_construct_data, use_page, "Configuring Devs", docs.usage.config, 350)
load_text(false, "Manual Control")
local man_ctrl_page = guide_section(sect_construct_data, use_page, "Manual Control", docs.usage.manual, 100)
load_text(false, "Auto Control")
local auto_ctrl_page = guide_section(sect_construct_data, use_page, "Auto Control", docs.usage.auto, 200)
load_text(false, "Waste Control")
local waste_ctrl_page = guide_section(sect_construct_data, use_page, "Waste Control", docs.usage.waste, 120)
PushButton{parent=use,y=3,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=conn_dev_page.nav_to}
PushButton{parent=use,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=config_dev_page.nav_to}
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=man_ctrl_page.nav_to}
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=auto_ctrl_page.nav_to}
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=waste_ctrl_page.nav_to}
load_text("Operator UIs")
PushButton{parent=use,y=3,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
TextBox{parent=uis,y=1,text="Operator UIs",alignment=ALIGN.CENTER}
PushButton{parent=uis,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
@ -206,84 +188,51 @@ local function new_view(root)
local annunc_div = Div{parent=page_div,x=2}
table.insert(panes, annunc_div)
local coord_page = app.new_page(uis_page, #panes + 1)
local coord_div = Div{parent=page_div,x=2}
table.insert(panes, coord_div)
load_text(false, "Alarms")
local alarms_page = guide_section(sect_construct_data, uis_page, "Alarms", docs.alarms, 100)
PushButton{parent=uis,y=3,text="Alarms >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=alarms_page.nav_to}
PushButton{parent=uis,text="Annunciators >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=annunc_page.nav_to}
PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=coord_page.nav_to}
load_text(false, "Annunciators")
PushButton{parent=uis,text="Pocket UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
TextBox{parent=annunc_div,y=1,text="Annunciators",alignment=ALIGN.CENTER}
PushButton{parent=annunc_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
PushButton{parent=annunc_div,y=3,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
PushButton{parent=annunc_div,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
PushButton{parent=annunc_div,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
PushButton{parent=annunc_div,text="Unit RCS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rcs_page.nav_to}
load_text(false, "Coordinator UI")
TextBox{parent=coord_div,y=1,text="Coordinator UI",alignment=ALIGN.CENTER}
PushButton{parent=coord_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
load_text(false, "Main Display")
local main_disp_page = guide_section(sect_construct_data, coord_page, "Main Display", docs.c_ui.main, 300)
load_text(false, "Flow Display")
local flow_disp_page = guide_section(sect_construct_data, coord_page, "Flow Display", docs.c_ui.flow, 210)
load_text(false, "Unit Displays")
local unit_disp_page = guide_section(sect_construct_data, coord_page, "Unit Displays", docs.c_ui.unit, 150)
PushButton{parent=coord_div,y=3,text="Main Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_disp_page.nav_to}
PushButton{parent=coord_div,text="Flow Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=flow_disp_page.nav_to}
PushButton{parent=coord_div,text="Unit Displays >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_disp_page.nav_to}
load_text("Front Panels")
PushButton{parent=annunc_div,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
PushButton{parent=annunc_div,text="Waste & Valves >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
load_text(false, "Common Items")
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
load_text(false, "Reactor PLC")
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 190)
load_text(false, "RTU Gateway")
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 180)
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
load_text(false, "Supervisor")
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
load_text(false, "Coordinator")
local fp_coordinator_page = guide_section(sect_construct_data, fps_page, "Coordinator", docs.fp.coordinator, 80)
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to}
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to}
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to}
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to}
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_coordinator_page.nav_to}
load_text("Glossary")
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140)
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 120)
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 100)
PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
load_text("Links")
TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER}
PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
@ -315,7 +264,7 @@ local function new_view(root)
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
app.delete_pages()
-- show loading screen

View File

@ -9,16 +9,16 @@ local conn_waiting = require("pocket.ui.components.conn_waiting")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local APP_ID = pocket.APP_ID
local LINK_STATE = iocontrol.LINK_STATE
-- create the connecting to SV & API page
---@param root Container parent
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
@ -32,29 +32,16 @@ local function create_pages(root)
local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
local function update()
local state = db.ps.get("link_state")
if state == LINK_STATE.UNLINKED then
root_pane.register(db.ps, "link_state", function (state)
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
root_pane.set_value(1)
elseif state == LINK_STATE.API_LINK_ONLY then
if not db.loader_require.sv then
root_pane.set_value(3)
db.nav.on_loader_connected()
else root_pane.set_value(1) end
elseif state == LINK_STATE.SV_LINK_ONLY then
if not db.loader_require.api then
root_pane.set_value(3)
db.nav.on_loader_connected()
else root_pane.set_value(2) end
root_pane.set_value(2)
else
root_pane.set_value(3)
db.nav.on_loader_connected()
end
end
root_pane.register(db.ps, "link_state", update)
root_pane.register(db.ps, "loader_reqs", update)
end)
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
end

View File

@ -1,337 +0,0 @@
--
-- Process Control App
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local process = require("pocket.process")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local HazardButton = require("graphics.elements.controls.HazardButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local NumberField = require("graphics.elements.form.NumberField")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local text_fg = style.text_fg
local field_fg_bg = style.field
local field_dis_fg_bg = style.field_disable
local red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
local grn_ind_s = style.icon_states.grn_ind_s
local wht_ind_s = style.icon_states.wht_ind_s
local hzd_fg_bg = style.hzd_fg_bg
local dis_colors = cpair(colors.white, colors.lightGray)
-- new process control page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.PROCESS, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.purple,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local f_ps = db.facility.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- create all page divs
for _ = 1, db.facility.num_units + 3 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_proc()
last_update = util.time_ms()
end
end
--#region unit settings/status
local rate_limits = {} ---@type NumberField[]
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i]
local u_ps = unit.unit_ps
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
TextBox{parent=u_div,y=3,text="Auto Rate Limit",fg_bg=label_fg_bg}
rate_limits[i] = NumberField{parent=u_div,x=1,y=4,width=16,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
TextBox{parent=u_div,x=18,y=4,text="mB/t",width=4,fg_bg=label_fg_bg}
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
local ready = IconIndicator{parent=u_div,y=6,label="Auto Ready",states=grn_ind_s}
local a_stb = IconIndicator{parent=u_div,label="Auto Standby",states=wht_ind_s}
local degraded = IconIndicator{parent=u_div,label="Unit Degraded",states=red_ind_s}
ready.register(u_ps, "U_AutoReady", ready.update)
degraded.register(u_ps, "U_AutoDegraded", degraded.update)
-- update standby indicator
a_stb.register(u_ps, "status", function (active)
a_stb.update(unit.annunciator.AutoControl and (not active))
end)
a_stb.register(u_ps, "AutoControl", function (auto_active)
if auto_active then
a_stb.update(unit.reactor_data.mek_status.status == false)
else a_stb.update(false) end
end)
local function _set_group(value) process.set_group(i, value - 1) end
local group = RadioButton{parent=u_div,y=10,options=types.AUTO_GROUP_NAMES,callback=_set_group,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
-- can't change group if auto is engaged regardless of if this unit is part of auto control
group.register(f_ps, "auto_active", function (auto_active)
if auto_active then group.disable() else group.enable() end
end)
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
TextBox{parent=u_div,y=16,text="Assigned Group",fg_bg=style.label}
local auto_grp = TextBox{parent=u_div,text="Manual",width=11,fg_bg=text_fg}
auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
util.nop()
end
--#endregion
--#region process control options page
local o_pane = panes[db.facility.num_units + 2]
local o_div = Div{parent=o_pane,x=2,width=main.get_width()-2}
local opt_page = app.new_page(nil, db.facility.num_units + 2)
opt_page.tasks = { update }
TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER}
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}
mode.register(f_ps, "process_mode", mode.set_value)
TextBox{parent=o_div,y=9,text="Burn Rate Target",fg_bg=label_fg_bg}
local b_target = NumberField{parent=o_div,x=1,y=10,width=15,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
TextBox{parent=o_div,x=17,y=10,text="mB/t",fg_bg=label_fg_bg}
TextBox{parent=o_div,y=12,text="Charge Level Target",fg_bg=label_fg_bg}
local c_target = NumberField{parent=o_div,x=1,y=13,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
TextBox{parent=o_div,x=17,y=13,text="M"..db.energy_label,fg_bg=label_fg_bg}
TextBox{parent=o_div,y=15,text="Generation Target",fg_bg=label_fg_bg}
local g_target = NumberField{parent=o_div,x=1,y=16,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
TextBox{parent=o_div,x=17,y=16,text="k"..db.energy_label.."/t",fg_bg=label_fg_bg}
b_target.register(f_ps, "process_burn_target", b_target.set_value)
c_target.register(f_ps, "process_charge_target", c_target.set_value)
g_target.register(f_ps, "process_gen_target", g_target.set_value)
--#endregion
--#region process control page
local c_pane = panes[db.facility.num_units + 1]
local c_div = Div{parent=c_pane,x=2,width=main.get_width()-2}
local proc_ctrl = app.new_page(nil, db.facility.num_units + 1)
proc_ctrl.tasks = { update }
TextBox{parent=c_div,y=1,text="Process Control",alignment=ALIGN.CENTER}
local u_stat = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,x=1,y=3,fg_bg=cpair(colors.black,colors.lightGray)}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",alignment=ALIGN.CENTER}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",height=2,alignment=ALIGN.CENTER,trim_whitespace=true,fg_bg=cpair(colors.gray,colors.lightGray)}
stat_line_1.register(f_ps, "status_line_1", stat_line_1.set_value)
stat_line_2.register(f_ps, "status_line_2", stat_line_2.set_value)
local function _start_auto()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_numeric() end
process.process_start(mode.get_value(), b_target.get_numeric(), db.energy_convert_to_fe(c_target.get_numeric()),
db.energy_convert_to_fe(g_target.get_numeric()), limits)
end
local start = HazardButton{parent=c_div,x=2,y=9,text="START",accent=colors.lightBlue,callback=_start_auto,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
local stop = HazardButton{parent=c_div,x=13,y=9,text="STOP",accent=colors.red,callback=process.process_stop,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
db.facility.start_ack = start.on_response
db.facility.stop_ack = stop.on_response
start.register(f_ps, "auto_ready", function (ready)
if ready and (not db.facility.auto_active) then start.enable() else start.disable() end
end)
local auto_ready = IconIndicator{parent=c_div,y=14,label="Units Ready",states=grn_ind_s}
local auto_act = IconIndicator{parent=c_div,label="Process Active",states=grn_ind_s}
local auto_ramp = IconIndicator{parent=c_div,label="Process Ramping",states=wht_ind_s}
local auto_sat = IconIndicator{parent=c_div,label="Min/Max Burn Rate",states=yel_ind_s}
auto_ready.register(f_ps, "auto_ready", auto_ready.update)
auto_act.register(f_ps, "auto_active", auto_act.update)
auto_ramp.register(f_ps, "auto_ramping", auto_ramp.update)
auto_sat.register(f_ps, "auto_saturated", auto_sat.update)
-- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree
-- when it comes to unloading the process app, child elements will not directly be registered here
-- (preventing garbage collection until the parent 'page_div' is deleted)
page_div.register(f_ps, "auto_active", function (active)
if active then
b_target.disable()
c_target.disable()
g_target.disable()
mode.disable()
start.disable()
for i = 1, #rate_limits do rate_limits[i].disable() end
else
b_target.enable()
c_target.enable()
g_target.enable()
mode.enable()
if db.facility.auto_ready then start.enable() end
for i = 1, #rate_limits do rate_limits[i].enable() end
end
end)
--#endregion
--#region auto-SCRAM annunciator page
local a_pane = panes[db.facility.num_units + 3]
local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}
local annunc_page = app.new_page(nil, db.facility.num_units + 3)
annunc_page.tasks = { update }
TextBox{parent=a_div,y=1,text="Automatic SCRAM",alignment=ALIGN.CENTER}
local auto_scram = IconIndicator{parent=a_div,y=3,label="Automatic SCRAM",states=red_ind_s}
TextBox{parent=a_div,y=5,text="Induction Matrix",fg_bg=label_fg_bg}
local matrix_flt = IconIndicator{parent=a_div,label="Matrix Fault",states=yel_ind_s}
local matrix_fill = IconIndicator{parent=a_div,label="Charge High",states=red_ind_s}
TextBox{parent=a_div,y=9,text="Assigned Units",fg_bg=label_fg_bg}
local unit_crit = IconIndicator{parent=a_div,label="Critical Alarm",states=red_ind_s}
TextBox{parent=a_div,y=12,text="Facility",fg_bg=label_fg_bg}
local fac_rad_h = IconIndicator{parent=a_div,label="Radiation High",states=red_ind_s}
TextBox{parent=a_div,y=15,text="Generation Rate Mode",fg_bg=label_fg_bg}
local gen_fault = IconIndicator{parent=a_div,label="Control Fault",states=yel_ind_s}
auto_scram.register(f_ps, "auto_scram", auto_scram.update)
matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)
gen_fault.register(f_ps, "as_gen_fault", gen_fault.update)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x17 ", color = core.cpair(colors.black, colors.purple), callback = proc_ctrl.nav_to },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = annunc_page.nav_to },
{ label = "OPT", color = core.cpair(colors.black, colors.yellow), callback = opt_page.nav_to }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
end
app.set_sidebar(list)
-- done, show the app
proc_ctrl.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -1,219 +0,0 @@
--
-- Radiation Monitor App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
-- new radiation monitor page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.RADMON, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.yellow,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local f_ps = db.facility.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- create all page divs
for _ = 1, db.facility.num_units + 2 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_rad()
last_update = util.time_ms()
end
end
-- create a new radiation monitor list
---@param parent Container
---@param ps psil
local function new_mon_list(parent, ps)
local mon_list = ListBox{parent=parent,y=6,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local elem_list = {} ---@type graphics_element[]
mon_list.register(ps, "radiation_monitors", function (data)
local ids = textutils.unserialize(data)
-- delete any disconnected monitors
for id, elem in pairs(elem_list) do
if not util.table_contains(ids, id) then
elem.delete()
elem_list[id] = nil
end
end
-- add newly connected monitors
for _, id in pairs(ids) do
if not elem_list[id] then
elem_list[id] = Div{parent=mon_list,height=5}
local mon_rect = Rectangle{parent=elem_list[id],height=4,x=2,width=20,border=border(1,colors.gray,true),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
TextBox{parent=mon_rect,text="Env. Detector "..id}
local mon_rad = RadIndicator{parent=mon_rect,x=2,label="",format="%13.3f",lu_colors=cpair(colors.gray,colors.gray),width=18}
mon_rad.register(ps, "radiation@" .. id, mon_rad.update)
end
end
end)
end
--#region unit radiation monitors
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane}
local unit = db.units[i]
local u_ps = unit.unit_ps
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Unit #"..i.." Monitors",alignment=ALIGN.CENTER}
TextBox{parent=u_div,x=2,y=3,text="Max Radiation",fg_bg=label_fg_bg}
local radiation = RadIndicator{parent=u_div,x=2,label="",format="%17.3f",lu_colors=lu_col,width=21}
radiation.register(u_ps, "radiation", radiation.update)
new_mon_list(u_div, u_ps)
end
--#endregion
--#region overview page
local s_pane = panes[db.facility.num_units + 1]
local s_div = Div{parent=s_pane,x=2,width=main.get_width()-2}
local stat_page = app.new_page(nil, db.facility.num_units + 1)
stat_page.tasks = { update }
TextBox{parent=s_div,y=1,text=" Radiation Monitoring",alignment=ALIGN.CENTER}
TextBox{parent=s_div,y=3,text="Max Facility Rad.",fg_bg=label_fg_bg}
local s_f_rad = RadIndicator{parent=s_div,label="",format="%17.3f",lu_colors=lu_col,width=21}
s_f_rad.register(f_ps, "radiation", s_f_rad.update)
for i = 1, db.facility.num_units do
local unit = db.units[i]
local u_ps = unit.unit_ps
s_div.line_break()
TextBox{parent=s_div,text="Max Unit "..i.." Radiation",fg_bg=label_fg_bg}
local s_u_rad = RadIndicator{parent=s_div,label="",format="%17.3f",lu_colors=lu_col,width=21}
s_u_rad.register(u_ps, "radiation", s_u_rad.update)
end
--#endregion
--#region overview page
local f_pane = panes[db.facility.num_units + 2]
local f_div = Div{parent=f_pane,width=main.get_width()}
local fac_page = app.new_page(nil, db.facility.num_units + 2)
fac_page.tasks = { update }
TextBox{parent=f_div,y=1,text="Facility Monitors",alignment=ALIGN.CENTER}
TextBox{parent=f_div,x=2,y=3,text="Max Radiation",fg_bg=label_fg_bg}
local f_rad = RadIndicator{parent=f_div,x=2,label="",format="%17.3f",lu_colors=lu_col,width=21}
f_rad.register(f_ps, "radiation", f_rad.update)
new_mon_list(f_div, f_ps)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x1e ", color = core.cpair(colors.black, colors.blue), callback = stat_page.nav_to },
{ label = "FAC", color = core.cpair(colors.black, colors.yellow), callback = fac_page.nav_to }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
end
app.set_sidebar(list)
-- done, show the app
stat_page.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -1,5 +1,5 @@
--
-- About Page
-- System Apps
--
local comms = require("scada-common.comms")
@ -12,33 +12,37 @@ local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.PushButton")
local PushButton = require("graphics.elements.controls.push_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create about page view
---@param root Container parent
-- create system app pages
---@param root graphics_element parent
local function create_pages(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
----------------
-- About Page --
----------------
local app = db.nav.register_app(APP_ID.ABOUT, frame)
local about_root = Div{parent=root,x=1,y=1}
local about_page = app.new_page(nil, 1)
local nt_page = app.new_page(about_page, 2)
local fw_page = app.new_page(about_page, 3)
local hw_page = app.new_page(about_page, 4)
local about_app = db.nav.register_app(APP_ID.ABOUT, about_root)
local about = Div{parent=frame,x=1,y=2}
local about_page = about_app.new_page(nil, 1)
local nt_page = about_app.new_page(about_page, 2)
local fw_page = about_app.new_page(about_page, 3)
local hw_page = about_app.new_page(about_page, 4)
local about = Div{parent=about_root,x=1,y=2}
TextBox{parent=about,y=1,text="System Information",alignment=ALIGN.CENTER}
@ -54,7 +58,7 @@ local function create_pages(root)
local config = pocket.config
local nt_div = Div{parent=frame,x=1,y=2}
local nt_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",alignment=ALIGN.CENTER}
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@ -83,7 +87,7 @@ local function create_pages(root)
--#region Firmware Versions
local fw_div = Div{parent=frame,x=1,y=2}
local fw_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=fw_div,y=1,text="Firmware Versions",alignment=ALIGN.CENTER}
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@ -119,7 +123,7 @@ local function create_pages(root)
--#region Host Versions
local hw_div = Div{parent=frame,x=1,y=2}
local hw_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=hw_div,y=1,text="Host Versions",alignment=ALIGN.CENTER}
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@ -134,9 +138,9 @@ local function create_pages(root)
--#endregion
local root_pane = MultiPane{parent=frame,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
app.set_root_pane(root_pane)
about_app.set_root_pane(root_pane)
end
return create_pages

View File

@ -1,5 +1,5 @@
--
-- Unit Overview App
-- Unit Overview Page
--
local util = require("scada-common.util")
@ -9,32 +9,32 @@ local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local dyn_tank = require("pocket.ui.pages.dynamic_tank")
local boiler = require("pocket.ui.pages.unit_boiler")
local reactor = require("pocket.ui.pages.unit_reactor")
local turbine = require("pocket.ui.pages.unit_turbine")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local WaitingAnim = require("graphics.elements.animations.waiting")
local PushButton = require("graphics.elements.controls.PushButton")
local PushButton = require("graphics.elements.controls.push_button")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local text_fg = style.text_fg
-- local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local basic_states = style.icon_states.basic_states
local mode_states = style.icon_states.mode_states
local red_ind_s = style.icon_states.red_ind_s
@ -47,7 +47,7 @@ local emc_ind_s = {
}
-- new unit page view
---@param root Container parent
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
@ -63,20 +63,20 @@ local function new_view(root)
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
local btn_fg_bg = cpair(colors.yellow, colors.black)
local btn_active = cpair(colors.white, colors.black)
local nav_links = {}
local page_div = nil ---@type Div|nil
local page_div = nil ---@type nil|graphics_element
-- set sidebar to display unit-specific fields based on a specified unit
local function set_sidebar(id)
local unit = db.units[id]
local unit = db.units[id] ---@type pioctl_unit
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = "U-" .. id, color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(id) end },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = nav_links[id].alarm },
{ label = "RPS", tall = true, color = core.cpair(colors.black, colors.cyan), callback = nav_links[id].rps },
@ -92,10 +92,6 @@ local function new_view(root)
table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].turbine[i] })
end
if #unit.tank_data_tbl > 0 then
table.insert(list, { label = "DYN", color = core.cpair(colors.black, colors.lightGray), callback = nav_links[id].d_tank })
end
app.set_sidebar(list)
end
@ -103,7 +99,7 @@ local function new_view(root)
local function load()
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
local panes = {}
local active_unit = 1
@ -131,7 +127,7 @@ local function new_view(root)
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i]
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- refresh data callback, every 500ms it will re-send the query
@ -312,6 +308,8 @@ local function new_view(root)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
-- rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Mismatches",alignment=ALIGN.CENTER,fg_bg=label}
local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s}
local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s}
local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s}
@ -321,6 +319,7 @@ local function new_view(root)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Aggregate Checks",alignment=ALIGN.CENTER,fg_bg=label}
if unit.num_boilers > 0 then
local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s}
@ -364,15 +363,6 @@ local function new_view(root)
--#endregion
--#region Dynamic Tank Tab
if #unit.tank_data_tbl > 0 then
local tank_pane = Div{parent=page_div}
nav_links[i].d_tank = dyn_tank(app, u_page, panes, tank_pane, i, unit.tank_ps_tbl[1], update)
end
--#endregion
util.nop()
end
@ -393,7 +383,7 @@ local function new_view(root)
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
app.delete_pages()
-- show loading screen

View File

@ -1,308 +0,0 @@
--
-- Waste Control App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local process = require("pocket.process")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local RadioButton = require("graphics.elements.controls.RadioButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local text_fg = style.text_fg
local lu_col = style.label_unit_pair
local yel_ind_s = style.icon_states.yel_ind_s
local wht_ind_s = style.icon_states.wht_ind_s
-- new waste control page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.WASTE, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.brown,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local f_ps = db.facility.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
local u_pages = {} ---@type nav_tree_page[]
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_waste()
last_update = util.time_ms()
end
end
--#region unit waste options/statistics
for i = 1, db.facility.num_units do
local u_pane = Div{parent=page_div}
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i]
local u_ps = unit.unit_ps
table.insert(panes, u_div)
local u_page = app.new_page(nil, #panes)
u_page.tasks = { update }
table.insert(u_pages, u_page)
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
local function set_waste(mode) process.set_unit_waste(i, mode) end
local waste_prod = StateIndicator{parent=u_div,x=16,y=3,states=style.get_waste().states_abbrv,value=1,min_width=6}
local waste_mode = RadioButton{parent=u_div,y=3,options=style.get_waste().unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
waste_prod.register(u_ps, "U_WasteProduct", waste_prod.update)
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
TextBox{parent=u_div,y=8,text="Plutonium (Pellets)",fg_bg=label_fg_bg}
local pu = DataIndicator{parent=u_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=u_div,y=11,text="Polonium",fg_bg=label_fg_bg}
local po = DataIndicator{parent=u_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=u_div,y=14,text="Polonium (Pellets)",fg_bg=label_fg_bg}
local popl = DataIndicator{parent=u_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
pu.register(u_ps, "pu_rate", pu.update)
po.register(u_ps, "po_rate", po.update)
popl.register(u_ps, "po_pl_rate", popl.update)
local sna_div = Div{parent=u_pane,x=2,width=page_div.get_width()-2}
table.insert(panes, sna_div)
local sps_page = app.new_page(u_page, #panes)
sps_page.tasks = { update }
PushButton{parent=u_div,x=6,y=18,text="SNA DATA",min_width=12,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=sps_page.nav_to}
PushButton{parent=sna_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=u_page.nav_to}
TextBox{parent=sna_div,y=1,text="Unit "..i.." SNAs",alignment=ALIGN.CENTER}
TextBox{parent=sna_div,y=3,text="Connected",fg_bg=label_fg_bg}
local count = DataIndicator{parent=sna_div,x=20,y=3,label="",format="%2d",value=0,unit="",lu_colors=lu_col,width=2,fg_bg=text_fg}
TextBox{parent=sna_div,y=5,text="Peak Possible Rate\n In\n Out",fg_bg=label_fg_bg}
local peak_i = DataIndicator{parent=sna_div,x=6,y=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
local peak_o = DataIndicator{parent=sna_div,x=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
TextBox{parent=sna_div,y=9,text="Current Maximum Rate\n In\n Out",fg_bg=label_fg_bg}
local max_i = DataIndicator{parent=sna_div,x=6,y=10,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
local max_o = DataIndicator{parent=sna_div,x=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
TextBox{parent=sna_div,y=13,text="Current Rate\n In\n Out",fg_bg=label_fg_bg}
local cur_i = DataIndicator{parent=sna_div,x=6,y=14,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
local cur_o = DataIndicator{parent=sna_div,x=6,label="",format="%11.2f",value=0,unit="mB/t",lu_colors=lu_col,width=17,fg_bg=text_fg}
count.register(u_ps, "sna_count", count.update)
peak_i.register(u_ps, "sna_peak_rate", function (x) peak_i.update(x * 10) end)
peak_o.register(u_ps, "sna_peak_rate", peak_o.update)
max_i.register(u_ps, "sna_max_rate", function (x) max_i.update(x * 10) end)
max_o.register(u_ps, "sna_max_rate", max_o.update)
cur_i.register(u_ps, "sna_out_rate", function (x) cur_i.update(x * 10) end)
cur_o.register(u_ps, "sna_out_rate", cur_o.update)
end
--#endregion
--#region waste control page
local c_pane = Div{parent=page_div}
local c_div = Div{parent=c_pane,x=2,width=main.get_width()-2}
table.insert(panes, c_div)
local wst_ctrl = app.new_page(nil, #panes)
wst_ctrl.tasks = { update }
TextBox{parent=c_div,y=1,text="Waste Control",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=c_div,x=3,y=3,states=style.get_waste().states,value=1,min_width=17}
local waste_prod = RadioButton{parent=c_div,y=5,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
status.register(f_ps, "current_waste_product", status.update)
waste_prod.register(f_ps, "process_waste_product", waste_prod.set_value)
local fb_active = IconIndicator{parent=c_div,y=9,label="Fallback Active",states=wht_ind_s}
local sps_disabled = IconIndicator{parent=c_div,y=10,label="SPS Disabled LC",states=yel_ind_s}
fb_active.register(f_ps, "pu_fallback_active", fb_active.update)
sps_disabled.register(f_ps, "sps_disabled_low_power", sps_disabled.update)
TextBox{parent=c_div,y=12,text="Nuclear Waste In",fg_bg=label_fg_bg}
local sum_raw_waste = DataIndicator{parent=c_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sum_raw_waste.register(f_ps, "burn_sum", sum_raw_waste.update)
TextBox{parent=c_div,y=15,text="Spent Waste Out",fg_bg=label_fg_bg}
local sum_sp_waste = DataIndicator{parent=c_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sum_sp_waste.register(f_ps, "spent_waste_rate", sum_sp_waste.update)
local stats_div = Div{parent=c_pane,x=2,width=page_div.get_width()-2}
table.insert(panes, stats_div)
local stats_page = app.new_page(wst_ctrl, #panes)
stats_page.tasks = { update }
PushButton{parent=c_div,x=6,y=18,text="PROD RATES",min_width=12,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=stats_page.nav_to}
PushButton{parent=stats_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=wst_ctrl.nav_to}
TextBox{parent=stats_div,y=1,text="Production Rates",alignment=ALIGN.CENTER}
TextBox{parent=stats_div,y=3,text="Plutonium (Pellets)",fg_bg=label_fg_bg}
local pu = DataIndicator{parent=stats_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=stats_div,y=6,text="Polonium",fg_bg=label_fg_bg}
local po = DataIndicator{parent=stats_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=stats_div,y=9,text="Polonium (Pellets)",fg_bg=label_fg_bg}
local popl = DataIndicator{parent=stats_div,label="",format="%16.3f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
pu.register(f_ps, "pu_rate", pu.update)
po.register(f_ps, "po_rate", po.update)
popl.register(f_ps, "po_pl_rate", popl.update)
TextBox{parent=stats_div,y=12,text="Antimatter",fg_bg=label_fg_bg}
local am = DataIndicator{parent=stats_div,label="",format="%16d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
am.register(f_ps, "sps_process_rate", function (r) am.update(r * 1000) end)
--#endregion
--#region waste options page
local o_pane = Div{parent=page_div}
local o_div = Div{parent=o_pane,x=2,width=main.get_width()-2}
table.insert(panes, o_pane)
local opt_page = app.new_page(nil, #panes)
opt_page.tasks = { update }
TextBox{parent=o_div,y=1,text="Waste Options",alignment=ALIGN.CENTER}
local pu_fallback = Checkbox{parent=o_div,x=2,y=3,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=o_div,x=2,y=5,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=label_fg_bg}
local lc_sps = Checkbox{parent=o_div,x=2,y=9,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=o_div,x=2,y=11,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=label_fg_bg}
pu_fallback.register(f_ps, "process_pu_fallback", pu_fallback.set_value)
lc_sps.register(f_ps, "process_sps_low_power", lc_sps.set_value)
--#endregion
--#region SPS page
local s_pane = Div{parent=page_div}
local s_div = Div{parent=s_pane,x=2,width=main.get_width()-2}
table.insert(panes, s_pane)
local sps_page = app.new_page(nil, #panes)
sps_page.tasks = { update }
TextBox{parent=s_div,y=1,text="Facility SPS",alignment=ALIGN.CENTER}
local sps_status = StateIndicator{parent=s_div,x=5,y=3,states=style.sps.states,value=1,min_width=12}
sps_status.register(db.facility.sps_ps_tbl[1], "SPSStateStatus", sps_status.update)
TextBox{parent=s_div,y=5,text="Input Rate",width=10,fg_bg=label_fg_bg}
local sps_in = DataIndicator{parent=s_div,label="",format="%16.2f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sps_in.register(f_ps, "po_am_rate", sps_in.update)
TextBox{parent=s_div,y=8,text="Production Rate",width=15,fg_bg=label_fg_bg}
local sps_rate = DataIndicator{parent=s_div,label="",format="%16d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
sps_rate.register(f_ps, "sps_process_rate", function (r) sps_rate.update(r * 1000) end)
--#endregion
-- setup multipane
local w_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(w_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = "WST", color = core.cpair(colors.black, colors.brown), callback = wst_ctrl.nav_to },
{ label = "OPT", color = core.cpair(colors.black, colors.white), callback = opt_page.nav_to },
{ label = "SPS", color = core.cpair(colors.black, colors.purple), callback = sps_page.nav_to }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = u_pages[i].nav_to })
end
app.set_sidebar(list)
-- done, show the app
wst_ctrl.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -8,17 +8,17 @@ local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local WaitingAnim = require("graphics.elements.animations.waiting")
local ALIGN = core.ALIGN
local cpair = core.cpair
-- create a waiting view
---@param parent Container parent
---@param parent graphics_element parent
---@param y integer y offset
local function init(parent, y, is_api)
-- root div
@ -29,15 +29,15 @@ local function init(parent, y, is_api)
local waiting_x = math.floor(parent.get_width() / 2) - 1
local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg),trim_whitespace=true}
local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg)}
if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg),trim_whitespace=true}
TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
msg.register(iocontrol.get_db().ps, "api_link_msg", msg.set_value)
else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg),trim_whitespace=true}
TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
msg.register(iocontrol.get_db().ps, "svr_link_msg", msg.set_value)
end

View File

@ -1,7 +1,3 @@
--
-- All the text documentation used in the Guide app is defined in this file.
--
local const = require("scada-common.constants")
local docs = {}
@ -11,9 +7,7 @@ local DOC_ITEM_TYPE = {
SECTION = 1,
SUBSECTION = 2,
TEXT = 3,
NOTE = 4,
TIP = 5,
LIST = 6
LIST = 4
}
---@enum DOC_LIST_TYPE
@ -57,18 +51,6 @@ local function text(body)
table.insert(target, item)
end
local function note(body)
---@class pocket_doc_note
local item = { type = DOC_ITEM_TYPE.NOTE, text = body }
table.insert(target, item)
end
local function tip(body)
---@class pocket_doc_tip
local item = { type = DOC_ITEM_TYPE.TIP, text = body }
table.insert(target, item)
end
---@param type DOC_LIST_TYPE
---@param items table
---@param colors table|nil colors for indicators or nil for normal lists
@ -78,140 +60,14 @@ local function list(type, items, colors)
table.insert(target, list_def)
end
--#region System Usage
docs.usage = {
conn = {}, config = {}, manual = {}, auto = {}, waste = {}
}
target = docs.usage.conn
sect("Overview")
tip("For the best setup experience, see the Wiki on GitHub or the YouTube channel! This app does not contain all information.")
text("Mekanism devices are connected to ComputerCraft computers that form the SCADA control system.")
sect("Mekanism Conns")
text("Multiblocks and single block devices are both connected directly to a computer by touching it or via wired modems.")
doc("usage_conn_mb", "Multiblocks", "For multiblocks, a logic adapter is used if it exists for that multiblock, otherwise a valve or port block is used.")
text("A wired modem is only connected to the block when you right click it and it gets a red border and you see a message in the chat with the peripheral name.")
tip("Do not connect all peripherals in the system on the same network cable, since Reactor PLCs will grab the first reactor they find and you may accidentally duplicate RTUs.")
sect("Computer Conns")
tip("It helps to be familiar with how ComputerCraft manages peripherals before using this system, though it is not necessary.")
doc("usage_conn_network", "Network", "All computers in the system communicate with each other via wireless or ender modems. Ender modems are preferred due to the unlimited range.")
text("Five different network channels are used and must have the same value for each name across all devices.")
text("For example, the supervisor channel SVR_CHANNEL must be set to the same channel for all devices in your system. Two different named channels should not share the same value (such as SVR_CHANNEL vs CRD_CHANNEL).")
doc("usage_conn_peri", "Peripherals", "ComputerCraft peripherals like monitors and speakers need to touch the computer or be connected via wired modems.")
target = docs.usage.config
sect("Overview")
tip("For the best setup experience, see the Wiki on GitHub or the YouTube channel! This app does not contain all information.")
text("All devices have a configurator program you can launch by running the 'configure' command.")
sect("Networking")
doc("usage_cfg_id", "Computer ID", "A computer ID must NEVER be the identical between devices, which can only happen if you duplicate a computer (such as if you middle-click on it and place it again in creative mode).")
doc("usage_cfg_chan", "Channels", "Channels are used for the computer to computer communication, described in the connection guide section. Channels with the same name must have the same value across all devices in your system and channels with different names cannot overlap.")
doc("usage_cfg_to", "Conn Timeout", "After this period of time the device will close the connection assuming the other device is unresponsive.")
doc("usage_cfg_tr", "Trusted Range", "Devices further than this block distance away will have any network traffic rejected by this device.")
doc("usage_cfg_auth", "Authentication", "To provide a level of security, you can enable facility-wide authentication by setting keys, which must be the same (and set) on all your devices. This adds computation time to each network transmission so you should only do this if you need it on multiplayer.")
sect("Logging")
text("Logs are automatically saved to a log.txt file in the root of the computer. You can change the path to it, if it contains verbose debug messages, and if it is appended to or overwritten each time the program runs.")
text("If you intend to be able to share logs, you should leave it to append.")
doc("usage_cfg_log_upload", "Sharing Logs", "To share logs, you would run 'pastebin put log.txt' where your log file is then share the code.")
sect("Reactor PLC")
text("The Reactor PLC must be connected to a single fission reactor that it will manage. Use the configurator to choose if you would like it to operate as networked or not.")
tip("The Reactor PLC should always be in a chunk with the reactor to ensure it can protect it on server start and/or chunk load.")
doc("usage_cfg_plc_nonet", "Non-Networked", "This lets you use this device as an advanced standalone safety system rather than a basic redstone breaker for easier safety protection.")
doc("usage_cfg_plc_net", "Networked", "This is the most commonly used mode. The Reactor PLC will require a connection to the Supervisor to operate and will allow usage through that for more advanced functionality.")
doc("usage_cfg_plc_unit", "Unit ID", "When networked, you can set any unit ID ranging from 1 to 4. Multiple Reactor PLCs cannot share the same unit ID.")
sect("RTU Gateway")
text("The RTU Gateway allows connecting multiple RTU interfaces to the SCADA system. These interfaces may be external peripherals or redstone.")
text("All devices except for fission reactors must be connected via an RTU Gateway.")
sect("Supervisor")
text("The Supervisor configuration is core to the entire system. If you change things about the system, such as the cooling devices or reactor count, it must be updated here.")
text("This configuration contains many settings that are detailed better in the configurator so they will not be covered here.")
doc("usage_cfg_sv_tanks", "Dynamic Tanks", "Dynamic tanks can be used to provide emergency coolant (and/or auxiliary coolant) to the system. Many layouts are supported by using a mix of facility tanks (connect to 1+ units) and unit tanks (connect to only one unit).")
doc("usage_cfg_sv_aux", "Auxiliary Coolant", "This coolant is enabled at the start of reactors to prevent water levels from dropping in the reactor or boiler while the turbine ramps up. This can be connected to a dynamic tank, a sink, or any other water supply.")
sect("Coordinator")
text("The Coordinator configuration is mainly focused around setting up your displays. This is best to do last after everything else. See the wiki on the GitHub for details on monitor sizing.")
tip("When changing the unit count on the Supervisor, you must also update it on the Coordinator.")
doc("usage_cfg_crd_main", "Main Monitor", "The main monitor contains the main interface and overview. It is always 8 block wide with varying height depending on how many units you have.")
doc("usage_cfg_crd_flow", "Flow Monitor", "The flow monitor contains the waste and coolant flow diagram. It is always 8 block wide with varying height depending on how many units you have.")
doc("usage_cfg_crd_unit", "Unit Monitor", "You need one unit monitor per reactor, and it is always a 4x4 monitor.")
text("Monitors can be connected by direct contact or via wired modems.")
text("Various unit and color options are available to customize the display to your liking. Using energy scales other than RF can impact the precision of your power-related auto control setpoints as RF is always used internally.")
sect("Pocket")
text("You're already here, so not much to mention!")
sect("Self-Check")
text("Most application configurators provide a self-check function that will check the validity of your configuration and the network connection. You should run this if you are having issues with that device.")
sect("Config Changes")
text("When an update adds or removes or otherwise modifies configuration requirements, you will be warned that you need to re-configure. You will not lose any prior data as updates will preserve configurations, you just need to step through the instructions again to add or change any new data.")
target = docs.usage.manual
sect("Overview")
text("Manual reactor control still includes safety checks and monitoring, but the burn rate is not automatically controlled.")
text("A unit is under manual control when the AUTO CTRL option Manual is selected on the unit display.")
note("Specific UIs will not be discussed here. If you need help with the UI, refer to Operator UIs > Coordinator UI > Unit Displays.")
sect("Manual Control")
text("The unit display on the Coordinator is used to run manual control. You may also start/stop and set the burn rate via the Mekanism UI on the Fission Reactor.")
tip("If some controls are grayed out on the unit display, that operation isn't currently available, such as due to the reactor being already started or being under auto control.")
text("Manual control is started by the START button and runs at the commanded burn rate next to it, which can be modified before starting or after having started by selecting a value then pressing SET.")
text("The reactor can be stopped via SCRAM, then the RPS needs to be reset via RESET.")
target = docs.usage.auto
sect("Overview")
text("A main feature of this system is automatic reactor control that supports various managed control modes.")
tip("You should first review the Main Display and Unit Display documentation under Operator UIs > Coordinator before proceeding if you are not familiar with the interfaces.")
sect("Configuration")
note("Configurations cannot be modified while auto control is active.")
doc("usage_auto_assign", "Unit Assignments", "Auto control only applies to units set to a mode other than Manual. To prefer certain units or only use the minimum number necessary, priority groups are used to split up the required burn rate.")
text("Primary units will be used first, followed by secondary, etc. If multiple are assigned to a group, burn rate will be assigned evenly between them.")
text("The next priority group will only be used once the previous one cannot keep up with the total required burn rate for auto control at that moment.")
doc("usage_auto_setpoints", "Setpoints", "Three setpoint spinner inputs are available for the three setpoint-based auto control modes. The system will do its best to meet the requested value, with the current value listed below the input.")
doc("usage_auto_limits", "Unit Limits", "Each unit can be limited to a maximum auto control burn rate to prevent exceeding any safe levels that you know of.")
doc("usage_auto_states", "Unit States", "Any assigned units must be shown as Ready and not Degraded to use auto control. See Operator UIs > Coordinator > Main Display for more.")
sect("Operation Modes")
text("Four auto control modes are available that function based on configurations set on the main display. All modes except Monitored Max Burn will try to only use the primary group until it can't keep up, then the secondary, etc.")
note("No units will be set to a burn rate higher than their limit.")
doc("usage_op_mon_max", "Monitored Max Burn", "This mode runs all units assigned to auto control at their unit limit burn rate regardless of priority group.")
doc("usage_op_com_rate", "Combined Burn Rate", "Assigned units will be commanded to meet the Burn Target setpoint.")
doc("usage_op_chg_level", "Charge Level", "Assigned units will be commanded to bring the induction matrix up to the requested Charge Target.")
doc("usage_op_gen_rate", "Generation Rate", "Assigned units will be commanded to maintain the requested Generation Target.")
note("The rate used is the input rate into the induction matrix, so using other power generation sources may disrupt this control mode.")
sect("Start and Stop")
text("A text box is used to indicate the system status. It will also provide information of why the system has paused control or failed to start.")
text("You cannot start auto control until all assigned units have all their devices connected and functional and the reactor's RPS is not tripped.")
doc("usage_op_save", "SAVE", "SAVE will save the configuration without starting control.")
doc("usage_op_start", "START", "START will attempt to start auto control, which includes first saving the configuration.")
doc("usage_op_stop", "STOP", "STOP will stop all reactors assigned to automatic control.")
target = docs.usage.waste
sect("Overview")
text("When 'valves' are connected for routing waste, this system can manage which waste product(s) are made. The flow monitor shows the diagram of how valves are meant to be connected.")
text("There are three waste products, listed below with the colors generally associated with them.")
list(DOC_LIST_TYPE.LED, { "Pu - Plutonium", "Po - Polonium", "AM - Antimatter" }, { colors.cyan, colors.green, colors.purple })
note("The Po and Pu colors are swapped in older versions of Mekanism.")
sect("Unit Waste")
text("Units can be set to specific waste products via buttons at the bottom right of a unit display.")
note("Refer to Operator UIs > Coordinator UI > Unit Displays for details.")
text("If 'Auto' is selected instead of a waste product, that unit's waste will be processed per the facility waste control.")
sect("Facility Waste")
text("Facility waste control adds additional functionality to waste processing through automatic control.")
text("The waste control interface on the main display lets you set a target waste type along with options that can change that based on circumstances.")
note("Refer to Operator UIs > Coordinator UI > Main Display for information on the display and control interface.")
doc("usage_waste_fallback", "Pu Fallback", "This option switches facility waste control to plutonium when the SNAs cannot keep up, such as at night.")
doc("usage_waste_sps_lc", "Low Charge SPS", "This option prevents the facility waste control from stopping antimatter production at low induction matrix charge (< 10%, resumes after reaching 15%).")
text("With that option enabled, antimatter production will continue. With it disabled, it will switch to polonium if set to antimatter while charge is low.")
note("Pu Fallback takes priority and will switch to plutonium when appropriate regardless of the Low Charge SPS setting.")
--#endregion
--#region Operator UIs
--#region Alarms
-- important to note in the future: The PLC should always be in a chunk with the reactor to ensure it can protect it on chunk load if you do not keep it all chunk loaded
docs.alarms = {}
target = docs.alarms
doc("ContainmentBreach", "Containment Breach", "Reactor disconnected or indicated unformed while being at or above 100% damage; explosion assumed.")
doc("ContainmentRadiation", "Containment Radiation", "Environment detector(s) assigned to the unit have observed high levels of radiation.")
doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the Supervisor.")
doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the supervisor.")
doc("CriticalDamage", "Damage Critical", "Reactor damage has reached or exceeded 100%, so it will explode at any moment.")
doc("ReactorDamage", "Reactor Damage", "Reactor temperature causing increasing damage to the reactor casing.")
doc("ReactorOverTemp", "Reactor Over Temp", "Reactor temperature is at or above maximum safe temperature, so it is now taking damage.")
@ -222,10 +78,6 @@ doc("RPSTransient", "RPS Transient", "Reactor protection system was activated.")
doc("RCSTransient", "RCS Transient", "Something is wrong with the reactor coolant system, check RCS indicators for details.")
doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due to having full energy storage. This will prevent cooling, so it needs to be resolved before using that unit.")
--#endregion
--#region Annunciators
docs.annunc = {
unit = {
main_section = {}, rps_section = {}, rcs_section = {}
@ -237,8 +89,8 @@ docs.annunc = {
target = docs.annunc.unit.main_section
sect("Unit Status")
doc("PLCOnline", "PLC Online", "Indicates if the fission Reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the Supervisor has stopped receiving data or a screen has frozen.")
doc("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.")
doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.")
doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.")
sect("Safety Status")
@ -246,7 +98,7 @@ doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is hol
doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.")
doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.")
doc("RadiationWarning", "Radiation Warning", "On if radiation levels are above normal. There is likely a leak somewhere, so that should be identified and fixed. Hazmat suit recommended.")
doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekanism. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.")
doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekansim. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.")
doc("RCSFlowLow", "RCS Flow Low", "Indicates if the reactor coolant system flow is low. This is observed when the cooled coolant level in the reactor is dropping. This can occur while a turbine spins up, but if it persists, check that the cooling system is operating properly. This can occur with smaller boilers or when using pipes and not having enough.")
doc("CoolantLevelLow", "Coolant Level Low", "On if the reactor coolant level is lower than it should be. Check the coolant system.")
doc("ReactorTempHigh", "Reactor Temp. High", "On if the reactor temperature is above expected maximum operating temperature. This is not yet damaging, but should be attended to. Check coolant system.")
@ -266,7 +118,7 @@ doc("high_temp", "Temperature High", "Indicates if the RPS tripped due to reachi
doc("low_cool", "Coolant Level Low Low", "Indicates if the RPS tripped due to very low coolant levels that result in the temperature uncontrollably rising. Ensure that the cooling system can provide sufficient cooled coolant flow.")
doc("no_fuel", "No Fuel", "Indicates if the RPS tripped due to no fuel being available. Check fuel input.")
doc("fault", "PPM Fault", "Indicates if the RPS tripped due to a peripheral access fault. Something went wrong interfacing with the reactor, try restarting the PLC.")
doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and Supervisor remain chunk loaded.")
doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and supervisor remain chunk loaded.")
doc("sys_fail", "System Failure", "Indicates if the RPS tripped due to the reactor not being formed. Ensure that the multi-block is formed.")
target = docs.annunc.unit.rcs_section
@ -278,7 +130,7 @@ doc("SteamFeedMismatch", "Steam Feed Mismatch", "There is an above tolerance dif
doc("MaxWaterReturnFeed", "Max Water Return Feed", "The turbines are condensing the max rate of water that they can per the structure build. If water return is insufficient, add more saturating condensers to your turbine(s).")
doc("WaterLevelLow", "Water Level Low", "The water level in the boiler is low. A larger boiler water tank may help, or you can feed additional water into the boiler from elsewhere.")
doc("HeatingRateLow", "Heating Rate Low", "The boiler is not hot enough to boil water, but it is receiving heated coolant. This is almost never a safety concern.")
doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the Supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.")
doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.")
doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity, but not tripped. You may need more turbines if they can't keep up.")
doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.")
doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.")
@ -296,150 +148,34 @@ doc("auto_ramping", "Process Ramping", "Automatic process control is performing
doc("auto_saturated", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
sect("Automatic SCRAM")
doc("auto_scram", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("as_matrix_fault", "Matrix Fault", "Automatic SCRAM occurred due to the loss of the induction matrix connection, or the matrix being unformed or faulted.")
doc("as_matrix_dc", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
doc("as_matrix_fill", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
doc("as_gen_fault", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
--#endregion
--#region Coordinator UI
docs.c_ui = {
main = {}, flow = {}, unit = {}
}
target = docs.c_ui.main
sect("Facility Diagram")
text("The facility overview diagram is made up of unit diagrams showing the reactor, boiler(s) if present, and turbine(s). This includes values of various key statistics such as temperatures along with bars showing the fill percentage of the tanks in each multiblock.")
text("Boilers are shown under the reactor, listed in order of index (#1 then #2 below). Turbines are shown to the right, also listed in order of index (indexes are per unit and set in the RTU Gateway configuration).")
text("Pipe connections are visualized with color-coded lines, which are primarily to indicate connections, as not all facilities may use pipes.")
note("If a component you have is not showing up, ensure the Supervisor is configured for your actual cooling configuration.")
sect("Facility Status")
note("The annunciator here is described in Operator UIs > Annunciators.")
doc("ui_fac_scram", "FAC SCRAM", "This SCRAMs all units in the facility.")
doc("ui_fac_ack", "ACK \x13", "This acknowledges (mutes) all alarms for all units in the facility.")
doc("ui_fac_rad", "Radiation", "The facility radiation, which is the current maximum of all connected facility radiation monitors (excludes unit monitors).")
doc("ui_fac_linked", "Linked RTUs", "The number of RTU Gateways connected.")
sect("Automatic Control")
text("This interface is used for managing automatic facility control, which only applies to units set via the unit display to be under auto control. This includes setpoints, status, configuration, and control.")
doc("ui_fac_auto_bt", "Burn Target", "When set to Combined Burn Rate mode, assigned units will ramp up to meet this combined target.")
doc("ui_fac_auto_ct", "Charge Target", "When set to Charge Level mode, assigned units will run to reach and maintain this induction matrix charge level.")
doc("ui_fac_auto_gt", "Gen. Target", "When set to Generation Rate mode, assigned units will run to reach and maintain this continuous power output, using the induction matrix input rate.")
doc("ui_fac_save", "SAVE", "This saves your configuration without starting control.")
doc("ui_fac_start", "START", "This starts the configured automatic control.")
tip("START also includes the SAVE operation.")
doc("ui_fac_stop", "STOP", "This terminates automatic control, stopping assigned units.")
text("There are four automatic control modes, detailed further in System Usage > Automatic Control")
doc("ui_fac_auto_mmb", "Monitored Max Burn", "This runs all assigned units at the maximum configured rate.")
doc("ui_fac_auto_cbr", "Combined Burn Rate", "This runs assigned units to meet the target combined rate.")
doc("ui_fac_auto_cl", "Charge Level", "This runs assigned units to maintain an induction matrix charge level.")
doc("ui_fac_auto_gr", "Generation Rate", "This runs assigned units to meet a target induction matrix power input rate.")
doc("ui_fac_auto_lim", "Unit Limit", "Each unit can have a limit set that auto control will never exceed.")
doc("ui_fac_unit_ready", "Unit Status Ready", "A unit is only ready for auto control if all multiblocks are formed, online with data received, and there is no RPS trip.")
doc("ui_fac_unit_degraded", "Unit Status Degraded", "A unit is degraded if the reactor, boiler(s), and/or turbine(s) are faulted or not connected.")
sect("Waste Control")
text("Above unit statuses are the unit waste statuses, showing which are set to the auto waste mode and the actual current waste production of that unit.")
text("The facility automatic waste control interface is surrounded by a brown border and lets you configure that system, starting with the requested waste product.")
doc("ui_fac_waste_pu_fall_act", "Fallback Active", "When the system is falling back to plutonium production while SNAs cannot keep up.")
doc("ui_fac_waste_sps_lc_act", "SPS Disabled LC", "When the system is falling back to polonium production to prevent draining all power with the SPS while the induction matrix charge has dropped below 10% and not yet reached 15%.")
doc("ui_fac_waste_pu_fall", "Pu Fallback", "Switch from Po or Antimatter when the SNAs can't keep up (like at night).")
doc("ui_fac_waste_sps_lc", "Low Charge SPS", "Continue running antimatter production even at low induction matrix charge levels (<10%).")
sect("Induction Matrix")
text("The induction matrix statistics are shown at the bottom right, including fill bars for the FILL, I (input rate), and O (output rate).")
text("Averages are computed by the system while other data is directly from the device.")
doc("ui_fac_im_charge", "Charging", "Charge is increasing (more input than output).")
doc("ui_fac_im_charge", "Discharging", "Charge is draining (more output than input).")
doc("ui_fac_im_charge", "Max I/O Rate", "The induction providers are at their maximum rate.")
doc("ui_fac_eta", "ETA", "The ETA is based off a longer average so it may take a minute to stabilize, but will give a rough estimate of time to charge/discharge.")
target = docs.c_ui.flow
sect("Flow Diagram")
text("The coolant and waste flow monitor is one large P&ID (process and instrumentation diagram) showing an overview of those flows.")
text("Color-coded pipes are used to show the connections, and valve symbols \x10\x11 are used to show valves (redstone controlled pipes).")
doc("ui_flow_rates", "Flow Rates", "Flow rates are always shown below their respective pipes and sourced from devices when possible. The waste flow is based on the reactor burn rate, then everything downstream of the SNAs are based on the SNA production rate.")
doc("ui_flow_valves", "Standard Valves", "Valve naming (PV00-XX) is based on P&ID naming conventions. These count up across the whole facility, and use tags at the end to add clarity.")
note("The indicator next to the label turns on when the associated redstone RTU is connected.")
list(DOC_LIST_TYPE.BULLET, { "PU: Plutonium", "PO: Polonium", "PL: Po Pellets", "AM: Antimatter", "EMC: Emer. Coolant", "AUX: Aux. Coolant" })
doc("ui_flow_valve_open", "OPEN", "This indicates if the respective valve is commanded open.")
doc("ui_flow_prv", "PRVs", "Pressure Relief Valves (PRVs) are used to show the turbine steam dumping states of each turbine.")
list(DOC_LIST_TYPE.LED, { "Not Dumping", "Dumping Excess", "Dumping" }, { colors.gray, colors.yellow, colors.red })
sect("SNAs")
text("Solar Neutron Activators are shown on the flow diagram as a combined block due to the large variable count supported.")
tip("SNAs consume 10x the waste as they produce in antimatter, so take that into account before connecting too many SNAs.")
doc("ui_flow_sna_act", "ACTIVE", "The SNAs have a non-zero total flow.")
doc("ui_flow_sna_cnt", "CNT", "The count of SNAs assigned to the unit.")
doc("ui_flow_sna_peak_o", "PEAK\x1a", "The combined theoretical peak output the SNAs can achieve under full sunlight.")
doc("ui_flow_sna_max_o", "MAX \x1a", "The current combined maximum output rate of the SNAs (based on current sunlight).")
doc("ui_flow_sna_max_i", "\x1aMAX", "The computed combined maximum input rate (10x the output rate).")
doc("ui_flow_sna_in", "\x1aIN", "The current input rate into the SNAs.")
sect("Dynamic Tanks")
text("Dynamic tanks configured for the system are listed to the left. The title may start with U for unit tanks or F for facility tanks.")
text("The fill information and water level are shown below the status label.")
doc("ui_flow_dyn_fill", "FILL", "If filling is enabled by the tank mode (via Mekanism UI).")
doc("ui_flow_dyn_empty", "EMPTY", "If emptying is enabled by the tank mode (via Mekanism UI).")
sect("SPS")
doc("ui_flow_sps_in", "Input Rate", "The rate of polonium into the SPS.")
doc("ui_flow_sps_prod", "Production Rate", "The rate of antimatter produced by the SPS.")
sect("Statistics")
text("The sum of all unit's waste rate statistics are shown under the SPS block. These are combined current rates, not long-term sums.")
doc("ui_flow_stat_raw", "RAW WASTE", "The combined rate of raw waste generated by the reactors before processing.")
doc("ui_flow_stat_proc", "PROC. WASTE", "The combined rates of different waste product production. Pu is plutonium, Po is polonium, and PoPl is polonium pellets. Antimatter is shown in the SPS block.")
doc("ui_flow_stat_spent", "SPENT WASTE", "The combined rate of spent waste generated after processing.")
sect("Other Blocks")
text("Other blocks, such as CENTRIFUGE, correspond to devices that are not intended to be connected and/or serve as labels.")
target = docs.c_ui.unit
sect("Data Display")
text("The unit monitor contains extensive data information, including annunciator and alarm displays described in the associated sections in the Operator UIs section.")
doc("ui_unit_core", "Core Map", "A core map diagram is shown at the top right, colored by core temperature. The layout is based off of the multiblock dimensions.")
list(DOC_LIST_TYPE.BULLET, { "Gray <= 300\xb0C", "Blue <= 350\xb0C", "Green < 600\xb0C", "Yellow < 100\xb0C", "Orange < 1200\xb0C", "Red < 1300\xb0C", "Pink >= 1300\xb0C" })
text("Internal tanks (fuel, cooled coolant, heated coolant, and waste) are displayed below the core map, labeled F, C, H, and W, respectively.")
doc("ui_unit_rad", "Radiation", "The unit radiation, which is the current maximum of all connected radiation monitors assigned to this unit.")
text("Multiple other data values are shown but should be self-explanatory.")
sect("Controls")
text("A set of buttons and the burn rate input are used for manual reactor control. When in auto mode, unavailable controls are disabled. The burn rate is only applied after SET is pressed.")
doc("ui_unit_start", "START", "This starts the reactor at the requested burn rate.")
doc("ui_unit_scram", "SCRAM", "This SCRAMs the reactor.")
doc("ui_unit_ack", "ACK \x13", "This acknowledges alarms on this unit.")
doc("ui_unit_reset", "RESET", "This resets the RPS for this unit.")
sect("Auto Control")
text("To put this unit under auto control, select an option other than Manual. You must press SET to apply this, but cannot change this while auto control is active. The priorities available are described in System Usage > Automatic Control.")
doc("ui_unit_prio", "Prio. Group", "This displays the unit's auto control priority group.")
doc("ui_unit_ready", "READY", "This indicates if the unit is ready for auto control. A unit is only ready for auto control if all multiblocks are formed, online with data received, and there is no RPS trip.")
doc("ui_unit_standby", "STANDBY", "This indicates if the unit is set to auto control and that is active, but the auto control does not currently need this reactor to run at the moment, so it is idle.")
sect("Waste Processing")
text("The unit's waste output configuration can be set via these buttons. Auto will put this unit under control of the facility waste control, otherwise the system will always command the requested option for this unit.")
--#endregion
--#endregion
--#region Front Panels
docs.fp = {
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}, coordinator = {}
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}
}
--comp id "This must never be the identical between devices, and that can only happen if you duplicate a computer (such as middle-click on it and place it elsewhere in creative mode)."
target = docs.fp.common
sect("Core Status")
doc("fp_status", "STATUS", "This is always lit, except on the Reactor PLC (see Reactor PLC section).")
doc("fp_heartbeat", "HEARTBEAT", "This alternates between lit and unlit as the main loop on the device runs. If this freezes, something is wrong and the logs will indicate why.")
sect("Hardware & Network")
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the Supervisor's connection lists.")
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the supervisor's connection lists.")
doc("fp_modem", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.")
list(DOC_LIST_TYPE.LED, { "not linked", "linked", "link denied", "bad comms version", "duplicate PLC" }, { colors.gray, colors.green, colors.red, colors.orange, colors.yellow })
text("You can fix \"bad comms version\" by ensuring all devices are up-to-date, as this indicates a communications protocol version mismatch. Note that yellow is Reactor PLC-specific, indicating duplicate unit IDs in use.")
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the Supervisor.")
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the Supervisor and this device do not match. Make sure everything is up-to-date.")
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the supervisor.")
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the supervisor and this device do not match. Make sure everything is up-to-date.")
sect("Versions")
doc("fp_fw", "FW", "Firmware application version of this device.")
doc("fp_nt", "NT", "Network (comms) version this device has. These must match between devices in order for them to connect.")
target = docs.fp.r_plc
sect("Overview")
text("Documentation for Reactor PLC-specific front panel items are below. Refer to 'Common Items' for the items not covered in this section.")
sect("Core Status")
doc("fp_status", "STATUS", "This is green once the PLC is initialized and OK (has all its peripherals) and red if something is wrong, in which case you should refer to the other indicator lights (REACTOR & MODEM).")
sect("Hardware & Network")
@ -458,8 +194,8 @@ doc("fp_emer_cool", "EMER COOLANT", "This is only present if PLC-controlled emer
doc("fp_rps_trip", "RPS TRIP", "Flashes when the RPS has SCRAM'd the reactor due to a safety trip.")
sect("RPS Conditions")
doc("fp_rps_man", "MANUAL", "The RPS was tripped manually (SCRAM by user, not via the Mekanism Reactor UI).")
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the Supervisor automatically.")
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the Supervisor connection.")
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the supervisor automatically.")
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the supervisor connection.")
doc("fp_rps_pflt", "PLC FAULT", "The RPS tripped due to a peripheral error.")
doc("fp_rps_rflt", "RCT FAULT", "The RPS tripped due to the reactor not being formed.")
doc("fp_rps_temp", "HI DAMAGE", "The RPS tripped due to being >=" .. const.RPS_LIMITS.MAX_DAMAGE_PERCENT .. "% damaged.")
@ -470,9 +206,6 @@ doc("fp_rps_ccool", "LO CCOOLANT", "The RPS tripped due to having low levels of
doc("fp_rps_ccool", "HI HCOOLANT", "The RPS tripped due to having high levels of heated coolant (>" .. (const.RPS_LIMITS.MAX_HEATED_COLLANT_FILL * 100) .. "%).")
target = docs.fp.rtu_gw
sect("Overview")
text("Documentation for RTU Gateway-specific front panel items are below. Refer to 'Common Items' for the items not covered in this section.")
doc("fp_rtu_spkr", "SPEAKERS", "This is the count of speaker peripherals connected to this RTU Gateway.")
sect("Co-Routine States")
doc("fp_rtu_rt_main", "RT MAIN", "This indicates if the device's main loop co-routine is running.")
doc("fp_rtu_rt_comms", "RT COMMS", "This indicates if the communications handler co-routine is running.")
@ -485,49 +218,30 @@ doc("fp_rtu_rt", "Device Assignment", "In each RTU entry row, the device identif
target = docs.fp.supervisor
sect("Round Trip Times")
doc("fp_sv_rtt", "RTT", "Each connection has a round trip time, or RTT. Since the Supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
doc("fp_sv_fw", "RTT", "Each connection has a round trip time, or RTT. Since the supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=300ms", "yellow: <=500ms ", "red: >500ms" })
sect("SVR Tab")
text("This tab includes information about the Supervisor, covered by 'Common Items'.")
text("This tab includes information about the supervisor, covered by 'Common Items'.")
sect("PLC Tab")
text("This tab lists the expected PLC connections based on the number of configured units. Status information about each connection is shown when linked.")
doc("fp_sv_link", "LINK", "This indicates if the Reactor PLC is linked.")
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the Reactor PLC, or --- if disconnected.")
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the Reactor PLC.")
doc("fp_sv_link", "LINK", "This indicates if the reactor PLC is linked.")
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the reactor PLC, or --- if disconnected.")
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the reactor PLC.")
sect("RTU Tab")
text("As RTU gateways connect to the Supervisor, they will show up here along with some information.")
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU Gateway.")
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU Gateway (each line on the RTU Gateway's front panel).")
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU Gateway.")
text("As RTU gateways connect to the supervisor, they will show up here along with some information.")
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU gateway.")
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU gateway (each line on the RTU gateway's front panel).")
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU gateway.")
sect("PKT Tab")
text("As pocket computers connect to the Supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
text("As pocket computers connect to the supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
sect("DEV Tab")
text("If nothing is connected, this will list all the expected RTU devices that aren't found. This page should be blank if everything is connected and configured correctly. If not, it will list certain types of detectable problems.")
doc("fp_sv_d_miss", "MISSING", "These items list missing devices, with the details that should be used in the RTU's configuration.")
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the Supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
doc("fp_sv_d_dupe", "DUPLICATE", "If a device tries to connect that is configured the same as another, it will be rejected and show up here. If you try to connect two #1 turbines for a unit, that would fail and one would appear here.")
sect("INF Tab")
text("This tab gives information about the other tabs, along with extra details on the DEV tab.")
target = docs.fp.coordinator
sect("Round Trip Times")
doc("fp_crd_rtt", "RTT", "Each connection has a round trip time, or RTT. Since the Coordinator updates at a rate of 500ms, RTTs ~500ms - ~1000ms are typical. Higher RTTs indicate lag, which results in performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=1000ms", "yellow: <=1500ms ", "red: >1500ms" })
sect("CRD Tab")
text("This tab includes information about the Coordinator, partially covered by 'Common Items'.")
doc("fp_crd_spkr", "SPEAKER", "This indicates if the speaker is connected.")
doc("fp_crd_rt_main", "RT MAIN", "This indicates that the device's main loop co-routine is running.")
doc("fp_crd_rt_render", "RT RENDER", "This indicates that the Coordinator graphics renderer co-routine is running.")
doc("fp_crd_mon_main", "MAIN MONITOR", "The connection status of the main display monitor.")
doc("fp_crd_mon_flow", "FLOW MONITOR", "The connection status of the coolant and waste flow display monitor.")
doc("fp_crd_mon_unit", "UNIT X MONITOR", "The connection status of the monitor associated with a given unit.")
sect("API Tab")
text("This tab lists connected pocket computers. Refer to the Supervisor PKT tab documentation for details on fields.")
--#endregion
--#region Glossary
docs.glossary = {
abbvs = {}, terms = {}
}
@ -535,7 +249,8 @@ docs.glossary = {
target = docs.glossary.abbvs
doc("G_ACK", "ACK", "Alarm ACKnowledge. Pressing this acknowledges that you understand an alarm occurred and would like to stop the audio tone(s).")
doc("G_Auto", "Auto", "Automatic.")
doc("G_CRD", "CRD", "Coordinator. Abbreviation for the Coordinator computer.")
doc("G_CRD", "CRD", "Coordinator. Abbreviation for the coordinator computer.")
doc("G_DBG", "DBG", "Debug. Abbreviation for the debugging sessions from pocket computers found on the supervisor's front panel.")
doc("G_FP", "FP", "Front Panel. See Terminology.")
doc("G_Hi", "Hi", "High.")
doc("G_Lo", "Lo", "Low.")
@ -545,7 +260,7 @@ doc("G_PLC", "PLC", "Programmable Logic Controller. A device that not only repor
doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer created for this project that prevents peripheral calls from crashing applications.")
doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.")
doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the Reactor PLC responsible for keeping the reactor safe.")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.")
doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.")
doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.")
doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.")
@ -554,8 +269,6 @@ doc("G_UI", "UI", "User Interface.")
target = docs.glossary.terms
doc("G_AssignedUnit", "Assigned Unit", "A unit that is assigned to an automatic control group (not assigned to Manual).")
doc("G_AuxCoolant", "Auxiliary Coolant", "A separate water input to the reactor or boiler to supplement return water from a turbine during initial ramp-up.")
doc("G_EmerCoolant", "Emergency Coolant", "A dynamic tank or other water supply used when a reactor or boiler does not have enough water to stop a runaway reactor overheat.")
doc("G_Fault", "Fault", "Something has gone wrong and/or failed to function.")
doc("G_FrontPanel", "Front Panel", "A basic interface on the front of a device for viewing and sometimes modifying its state. This is what you see when looking at a computer running one of the SCADA applications.")
doc("G_HighHigh", "High High", "Very High.")
@ -569,6 +282,4 @@ doc("G_Tripped", "Tripped", "An alarm condition has been met, and is still met."
doc("G_Tripping", "Tripping", "Alarm condition(s) is/are met, but has/have not reached the minimum time before the condition(s) is/are deemed a problem.")
doc("G_TurbineTrip", "Turbine Trip", "The turbine stopped, which prevents heated coolant from being cooled. In Mekanism, this would occur when a turbine cannot generate any more energy due to filling its buffer and having no output with any remaining energy capacity.")
--#endregion
return docs

View File

@ -7,17 +7,13 @@ local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local about_app = require("pocket.ui.apps.about")
local alarm_app = require("pocket.ui.apps.alarm")
local comps_app = require("pocket.ui.apps.comps")
local control_app = require("pocket.ui.apps.control")
local facil_app = require("pocket.ui.apps.facility")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader")
local process_app = require("pocket.ui.apps.process")
local rad_app = require("pocket.ui.apps.radiation")
local sys_apps = require("pocket.ui.apps.sys_apps")
local unit_app = require("pocket.ui.apps.unit")
local waste_app = require("pocket.ui.apps.waste")
local home_page = require("pocket.ui.pages.home_page")
@ -25,16 +21,16 @@ local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local WaitingAnim = require("graphics.elements.animations.waiting")
local PushButton = require("graphics.elements.controls.PushButton")
local Sidebar = require("graphics.elements.controls.Sidebar")
local PushButton = require("graphics.elements.controls.push_button")
local Sidebar = require("graphics.elements.controls.sidebar")
local SignalBar = require("graphics.elements.indicators.SignalBar")
local SignalBar = require("graphics.elements.indicators.signal")
local ALIGN = core.ALIGN
local cpair = core.cpair
@ -42,12 +38,12 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create new main view
---@param main DisplayBox main displaybox
---@param main graphics_element main displaybox
local function init(main)
local db = iocontrol.get_db()
-- window header message and connection status
TextBox{parent=main,y=1,text=" S C ",fg_bg=style.header}
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
@ -67,16 +63,12 @@ local function init(main)
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
facil_app(page_div)
control_app(page_div)
process_app(page_div)
waste_app(page_div)
guide_app(page_div)
rad_app(page_div)
loader_app(page_div)
about_app(page_div)
alarm_app(page_div)
comps_app(page_div)
sys_apps(page_div)
diag_apps(page_div)
dummy_app(page_div)
-- verify all apps were created
assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
@ -86,7 +78,7 @@ local function init(main)
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
db.nav.go_home()
db.nav.open_app(APP_ID.ROOT)
-- done with initial render, lets go!
root_pane.set_value(2)

Some files were not shown because too many files have changed in this diff Show More