Compare commits
41 Commits
v1.5.0-bet
...
v1.6.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb7b5b4cb | ||
|
|
9bd79dacad | ||
|
|
c544d140bf | ||
|
|
353cb3622b | ||
|
|
b54f15bad6 | ||
|
|
4d9783beca | ||
|
|
5529774b0e | ||
|
|
2a541ef3fe | ||
|
|
e1b4d72ef8 | ||
|
|
6a0992c7a4 | ||
|
|
cff7c724be | ||
|
|
47bda73afe | ||
|
|
8daedc109c | ||
|
|
a164c18a50 | ||
|
|
4d663ada8d | ||
|
|
084a153a79 | ||
|
|
4ed6ec1c63 | ||
|
|
d3c2ba7bee | ||
|
|
55ff9dad4b | ||
|
|
0d6022f5e3 | ||
|
|
8b136d78a8 | ||
|
|
a5214730ef | ||
|
|
9f3ad3caf0 | ||
|
|
9bb2a99be5 | ||
|
|
65ace26258 | ||
|
|
61d975d13f | ||
|
|
1d7d6e9817 | ||
|
|
a2e0999cea | ||
|
|
1edee7f64b | ||
|
|
df61ec2c62 | ||
|
|
bf7a316b04 | ||
|
|
96c4444184 | ||
|
|
59eac62c33 | ||
|
|
ab193db153 | ||
|
|
7d65bba589 | ||
|
|
dcef5a96f0 | ||
|
|
ba0900ac65 | ||
|
|
8f54e95519 | ||
|
|
7b9824b6f9 | ||
|
|
b6835fc7d1 | ||
|
|
bc5a94cd3b |
56
README.md
56
README.md
@@ -7,6 +7,27 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||

|
||||

|
||||
|
||||
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
|
||||
|
||||

|
||||
|
||||
## Released Component Versions
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
Mod Requirements:
|
||||
- CC: Tweaked
|
||||
- Mekanism v10.1+
|
||||
@@ -16,32 +37,11 @@ Mod Recommendations:
|
||||
|
||||
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
|
||||
There was also an apparent bug with boilers disconnecting and reconnecting when active in my test world on 10.0.24, so it may not even have been an option to fully implement this with support for 10.0.
|
||||
|
||||
## Released Component Versions
|
||||
|
||||
### Core
|
||||
|
||||

|
||||

|
||||
|
||||
### Utilities
|
||||
|
||||

|
||||
|
||||
### Applications
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get eRz6cUNM ccmsi.lua`
|
||||
* `pastebin get RGasyTM4 ccmsi.lua`
|
||||
|
||||
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
|
||||
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
|
||||
@@ -86,14 +86,8 @@ A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication pr
|
||||
- Input Registers: Multi-Byte Read-Only (analog inputs)
|
||||
- Holding Registers: Multi-Byte Read/Write (analog I/O)
|
||||
|
||||
### Security and Encryption
|
||||
### Security
|
||||
|
||||
TBD, I am planning on AES symmetric encryption for security + HMAC to prevent replay attacks. This will be done utilizing this codebase: https://github.com/somesocks/lua-lockbox.
|
||||
HMAC message authentication is available as a configuration option to prevent replay attacks and generally prevent control or false data reporting within a system's network. This is done utilizing the [lua-lockbox](https://github.com/somesocks/lua-lockbox) project.
|
||||
|
||||
This is somewhat important here as otherwise anyone can just control your setup, which is undeseriable. Unlike normal Minecraft PVP chaos, it would be very difficult to identify who is messing with your system, as with an Ender Modem they can do it from effectively anywhere and the server operators would have to check every computer's filesystem to find suspicious code.
|
||||
|
||||
The other security mitigation for commanding (no effect on monitoring) is to enforce a maximum authorized transmission range, which has been added as a configurable feature.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None yet since the switch to requiring 10.1+!
|
||||
The other, simpler security feature is to enforce a maximum authorized transmission range, which is also a configurable feature on each device.
|
||||
|
||||
171
ccmsi.lua
171
ccmsi.lua
@@ -20,7 +20,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
local function println(message) print(tostring(message)) end
|
||||
local function print(message) term.write(tostring(message)) end
|
||||
|
||||
local CCMSI_VERSION = "v1.5a"
|
||||
local CCMSI_VERSION = "v1.7d"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
@@ -44,11 +44,15 @@ local function get_opt(opt, options)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- wait for any key to be pressed
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function any_key() os.pullEvent("key_up") end
|
||||
|
||||
-- ask the user yes or no
|
||||
local function ask_y_n(question, default)
|
||||
print(question)
|
||||
if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end
|
||||
local response = read()
|
||||
local response = read();any_key()
|
||||
if response == "" then return default
|
||||
elseif response == "Y" or response == "y" then return true
|
||||
elseif response == "N" or response == "n" then return false
|
||||
@@ -56,13 +60,13 @@ local function ask_y_n(question, default)
|
||||
end
|
||||
|
||||
-- print out a white + blue text message
|
||||
local function pkg_message(message, package) white(); print(message .. " "); blue(); println(package); white() end
|
||||
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
|
||||
|
||||
-- indicate actions to be taken based on package differences for installs/updates
|
||||
local function show_pkg_change(name, v_local, v_remote)
|
||||
if v_local ~= nil then
|
||||
if v_local ~= v_remote then
|
||||
print("[" .. name .. "] updating "); blue(); print(v_local); white(); print(" \xbb "); blue(); println(v_remote); white()
|
||||
print("[" .. name .. "] updating ");blue();print(v_local);white();print(" \xbb ");blue();println(v_remote);white()
|
||||
elseif mode == "install" then
|
||||
pkg_message("[" .. name .. "] reinstalling", v_local)
|
||||
end
|
||||
@@ -87,15 +91,13 @@ end
|
||||
local function get_remote_manifest()
|
||||
local response, error = http.get(install_manifest)
|
||||
if response == nil then
|
||||
orange(); println("failed to get installation manifest from GitHub, cannot update or install")
|
||||
red(); println("HTTP error: " .. error); white()
|
||||
orange();println("failed to get installation manifest from GitHub, cannot update or install")
|
||||
red();println("HTTP error: " .. error);white()
|
||||
return false, {}
|
||||
end
|
||||
|
||||
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end)
|
||||
if not ok then
|
||||
red(); println("error parsing remote installation manifest"); white()
|
||||
end
|
||||
if not ok then red();println("error parsing remote installation manifest");white() end
|
||||
|
||||
return ok, manifest
|
||||
end
|
||||
@@ -107,7 +109,7 @@ local function write_install_manifest(manifest, dependencies)
|
||||
local is_dependency = false
|
||||
for _, dependency in pairs(dependencies) do
|
||||
if (key == "bootloader" and dependency == "system") or key == dependency then
|
||||
is_dependency = true; break
|
||||
is_dependency = true;break
|
||||
end
|
||||
end
|
||||
if key == app or key == "comms" or is_dependency then versions[key] = value end
|
||||
@@ -120,6 +122,78 @@ local function write_install_manifest(manifest, dependencies)
|
||||
imfile.close()
|
||||
end
|
||||
|
||||
-- recursively build a tree out of the file manifest
|
||||
local function gen_tree(manifest)
|
||||
local function _tree_add(tree, split)
|
||||
if #split > 1 then
|
||||
local name = table.remove(split, 1)
|
||||
if tree[name] == nil then tree[name] = {} end
|
||||
table.insert(tree[name], _tree_add(tree[name], split))
|
||||
else return split[1] end
|
||||
return nil
|
||||
end
|
||||
|
||||
local list, tree = {}, {}
|
||||
|
||||
-- make a list of each and every file
|
||||
for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
|
||||
|
||||
for i = 1, #list do
|
||||
local split = {}
|
||||
string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
|
||||
if #split == 1 then table.insert(tree, list[i])
|
||||
else table.insert(tree, _tree_add(tree, split)) end
|
||||
end
|
||||
|
||||
return tree
|
||||
end
|
||||
|
||||
local function _in_array(val, array)
|
||||
for _, v in pairs(array) do if v == val then return true end end
|
||||
return false
|
||||
end
|
||||
|
||||
local function _clean_dir(dir, tree)
|
||||
if tree == nil then tree = {} end
|
||||
local ls = fs.list(dir)
|
||||
for _, val in pairs(ls) do
|
||||
local path = dir .. "/" .. val
|
||||
if fs.isDir(path) then
|
||||
_clean_dir(path, tree[val])
|
||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
|
||||
elseif not _in_array(val, tree) then
|
||||
fs.delete(path)
|
||||
println("deleted " .. path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- go through app/common directories to delete unused files
|
||||
local function clean(manifest)
|
||||
local root_ext = false
|
||||
local tree = gen_tree(manifest)
|
||||
|
||||
table.insert(tree, "install_manifest.json")
|
||||
table.insert(tree, "ccmsi.lua")
|
||||
table.insert(tree, "log.txt")
|
||||
|
||||
lgray()
|
||||
|
||||
local ls = fs.list("/")
|
||||
for _, val in pairs(ls) do
|
||||
if fs.isDir(val) then
|
||||
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
|
||||
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
|
||||
elseif not _in_array(val, tree) then
|
||||
root_ext = true
|
||||
yellow();println(val .. " not used")
|
||||
end
|
||||
end
|
||||
|
||||
white()
|
||||
if root_ext then println("Files in root directory won't be automatically deleted.") end
|
||||
end
|
||||
|
||||
-- get and validate command line options
|
||||
|
||||
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --")
|
||||
@@ -136,33 +210,33 @@ if #opts == 0 or opts[1] == "help" then
|
||||
println(" update - update files EXCEPT for config/logs")
|
||||
println(" remove - delete files EXCEPT for config/logs")
|
||||
println(" purge - delete files INCLUDING config/logs")
|
||||
white(); println("<app>"); lgray()
|
||||
white();println("<app>");lgray()
|
||||
println(" reactor-plc - reactor PLC firmware")
|
||||
println(" rtu - RTU firmware")
|
||||
println(" supervisor - supervisor server application")
|
||||
println(" coordinator - coordinator application")
|
||||
println(" pocket - pocket application")
|
||||
white(); println("<branch>"); yellow()
|
||||
white();println("<branch>");yellow()
|
||||
println(" second parameter when used with check")
|
||||
lgray(); println(" main (default) | latest | devel"); white()
|
||||
lgray();println(" main (default) | latest | devel");white()
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" })
|
||||
if mode == nil then
|
||||
red(); println("Unrecognized mode."); white()
|
||||
red();println("Unrecognized mode.");white()
|
||||
return
|
||||
end
|
||||
|
||||
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" })
|
||||
if app == nil and mode ~= "check" then
|
||||
red(); println("Unrecognized application."); white()
|
||||
red();println("Unrecognized application.");white()
|
||||
return
|
||||
end
|
||||
|
||||
-- determine target
|
||||
if mode == "check" then target = opts[2] else target = opts[3] end
|
||||
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
|
||||
if (target and target ~= "") then yellow(); println("Unknown target, defaulting to 'main'"); white() end
|
||||
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
|
||||
target = "main"
|
||||
end
|
||||
|
||||
@@ -179,7 +253,7 @@ if mode == "check" then
|
||||
|
||||
local local_ok, local_manifest = read_local_manifest()
|
||||
if not local_ok then
|
||||
yellow(); println("failed to load local installation information"); white()
|
||||
yellow();println("failed to load local installation information");white()
|
||||
local_manifest = { versions = { installer = CCMSI_VERSION } }
|
||||
else
|
||||
local_manifest.versions.installer = CCMSI_VERSION
|
||||
@@ -190,16 +264,16 @@ if mode == "check" then
|
||||
term.setTextColor(colors.purple)
|
||||
print(string.format("%-14s", "[" .. key .. "]"))
|
||||
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
||||
blue(); print(local_manifest.versions[key])
|
||||
blue();print(local_manifest.versions[key])
|
||||
if value ~= local_manifest.versions[key] then
|
||||
white(); print(" (")
|
||||
white();print(" (")
|
||||
term.setTextColor(colors.cyan)
|
||||
print(value); white(); println(" available)")
|
||||
else green(); println(" (up to date)") end
|
||||
print(value);white();println(" available)")
|
||||
else green();println(" (up to date)") end
|
||||
else
|
||||
lgray(); print("not installed"); white(); print(" (latest ")
|
||||
lgray();print("not installed");white();print(" (latest ")
|
||||
term.setTextColor(colors.cyan)
|
||||
print(value); white(); println(")")
|
||||
print(value);white();println(")")
|
||||
end
|
||||
end
|
||||
elseif mode == "install" or mode == "update" then
|
||||
@@ -218,7 +292,7 @@ elseif mode == "install" or mode == "update" then
|
||||
local local_ok, local_manifest = read_local_manifest()
|
||||
if not local_ok then
|
||||
if mode == "update" then
|
||||
red(); println("failed to load local installation information, cannot update"); white()
|
||||
red();println("failed to load local installation information, cannot update");white()
|
||||
return
|
||||
end
|
||||
else
|
||||
@@ -229,13 +303,13 @@ elseif mode == "install" or mode == "update" then
|
||||
ver.lockbox.v_local = local_manifest.versions.lockbox
|
||||
|
||||
if local_manifest.versions[app] == nil then
|
||||
red(); println("another application is already installed, please purge it before installing a new application"); white()
|
||||
red();println("another application is already installed, please purge it before installing a new application");white()
|
||||
return
|
||||
end
|
||||
|
||||
local_manifest.versions.installer = CCMSI_VERSION
|
||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||
yellow(); println("a newer version of the installer is available, it is recommended to download it"); white()
|
||||
yellow();println("a newer version of the installer is available, it is recommended to download it");white()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -265,7 +339,7 @@ elseif mode == "install" or mode == "update" then
|
||||
show_pkg_change("comms", ver.comms.v_local, ver.comms.v_remote)
|
||||
ver.comms.changed = ver.comms.v_local ~= ver.comms.v_remote
|
||||
if ver.comms.changed and ver.comms.v_local ~= nil then
|
||||
print("[comms] "); yellow(); println("other devices on the network will require an update"); white()
|
||||
print("[comms] ");yellow();println("other devices on the network will require an update");white()
|
||||
end
|
||||
|
||||
-- display graphics version change information
|
||||
@@ -277,7 +351,7 @@ elseif mode == "install" or mode == "update" then
|
||||
ver.lockbox.changed = ver.lockbox.v_local ~= ver.lockbox.v_remote
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue?", false) then return end
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
|
||||
--------------------------
|
||||
-- START INSTALL/UPDATE --
|
||||
@@ -302,7 +376,7 @@ elseif mode == "install" or mode == "update" then
|
||||
-- check space constraints
|
||||
if space_available < space_required then
|
||||
single_file_mode = true
|
||||
yellow(); println("WARNING: Insufficient space available for a full download!"); white()
|
||||
yellow();println("WARNING: Insufficient space available for a full download!");white()
|
||||
println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.")
|
||||
if mode == "update" then println("If installation still fails, delete this device's log file or uninstall the app (not purge) and try again.") end
|
||||
if not ask_y_n("Do you wish to continue?", false) then
|
||||
@@ -343,7 +417,7 @@ elseif mode == "install" or mode == "update" then
|
||||
local dl, err = http.get(repo_path .. file)
|
||||
|
||||
if dl == nil then
|
||||
red(); println("GET HTTP Error " .. err)
|
||||
red();println("GET HTTP Error " .. err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
@@ -384,10 +458,13 @@ elseif mode == "install" or mode == "update" then
|
||||
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
|
||||
red();println("Installation failed.")
|
||||
else orange();println("Update failed, existing files unmodified.") end
|
||||
end
|
||||
else
|
||||
-- go through all files and replace one by one
|
||||
@@ -405,7 +482,7 @@ elseif mode == "install" or mode == "update" then
|
||||
local dl, err = http.get(repo_path .. file)
|
||||
|
||||
if dl == nil then
|
||||
red(); println("GET HTTP Error " .. err)
|
||||
red();println("GET HTTP Error " .. err)
|
||||
success = false
|
||||
break
|
||||
else
|
||||
@@ -424,6 +501,9 @@ elseif mode == "install" or mode == "update" then
|
||||
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
|
||||
@@ -434,10 +514,10 @@ elseif mode == "install" or mode == "update" then
|
||||
elseif mode == "remove" or mode == "purge" then
|
||||
local ok, manifest = read_local_manifest()
|
||||
if not ok then
|
||||
red(); println("Error parsing local installation manifest."); white()
|
||||
red();println("Error parsing local installation manifest.");white()
|
||||
return
|
||||
elseif mode == "remove" and manifest.versions[app] == nil then
|
||||
red(); println(app .. " is not installed, cannot remove."); white()
|
||||
red();println(app .. " is not installed, cannot remove.");white()
|
||||
return
|
||||
end
|
||||
|
||||
@@ -449,7 +529,10 @@ elseif mode == "remove" or mode == "purge" then
|
||||
end
|
||||
|
||||
-- ask for confirmation
|
||||
if not ask_y_n("Continue?", false) then return end
|
||||
if not ask_y_n("Continue", false) then return end
|
||||
|
||||
-- delete unused files first
|
||||
clean(manifest)
|
||||
|
||||
local file_list = manifest.files
|
||||
local dependencies = manifest.depends[app]
|
||||
@@ -469,9 +552,9 @@ elseif mode == "remove" or mode == "purge" then
|
||||
end)
|
||||
|
||||
if not log_deleted then
|
||||
red(); println("failed to delete log file")
|
||||
white(); println("press enter to continue...")
|
||||
read(); lgray()
|
||||
red();println("failed to delete log file")
|
||||
white();println("press any key to continue...")
|
||||
any_key();lgray()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -480,10 +563,7 @@ elseif mode == "remove" or mode == "purge" then
|
||||
local files = file_list[dependency]
|
||||
for _, file in pairs(files) do
|
||||
if mode == "purge" or file ~= config_file then
|
||||
if fs.exists(file) then
|
||||
fs.delete(file)
|
||||
println("deleted " .. file)
|
||||
end
|
||||
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -508,8 +588,7 @@ elseif mode == "remove" or mode == "purge" then
|
||||
end
|
||||
|
||||
if folder ~= app and fs.isDir(folder) then
|
||||
fs.delete(folder)
|
||||
println("deleted app subdirectory " .. folder)
|
||||
fs.delete(folder);println("deleted app subdirectory " .. folder)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -527,7 +606,7 @@ elseif mode == "remove" or mode == "purge" then
|
||||
imfile.close()
|
||||
end
|
||||
|
||||
green(); println("Done!")
|
||||
green();println("Done!")
|
||||
end
|
||||
|
||||
white()
|
||||
|
||||
@@ -2,6 +2,7 @@ local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
@@ -12,7 +13,6 @@ local dialog = require("coordinator.ui.dialog")
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
@@ -22,6 +22,8 @@ local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_TIMEOUT = 60.0
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
-- request the user to select a monitor
|
||||
@@ -227,9 +229,12 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
sv_seq_num = 0,
|
||||
sv_r_seq_num = nil,
|
||||
sv_config_err = false,
|
||||
connected = false,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
last_api_est_acks = {}
|
||||
last_api_est_acks = {},
|
||||
est_start = 0,
|
||||
est_last = 0,
|
||||
est_tick_waiting = nil,
|
||||
est_task_done = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
@@ -295,77 +300,78 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
---@class coord_comms
|
||||
local public = {}
|
||||
|
||||
-- try to connect to the supervisor if not already linked
|
||||
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
|
||||
---@return boolean ok, boolean start_ui
|
||||
function public.try_connect(abort)
|
||||
local ok = true
|
||||
local start_ui = false
|
||||
|
||||
if not self.sv_linked then
|
||||
if self.est_tick_waiting == nil then
|
||||
self.est_start = util.time_s()
|
||||
self.est_last = self.est_start
|
||||
|
||||
self.est_tick_waiting, self.est_task_done =
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
|
||||
|
||||
_send_establish()
|
||||
else
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
|
||||
end
|
||||
|
||||
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
|
||||
self.est_task_done(false)
|
||||
|
||||
if abort then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
elseif not self.sv_linked then
|
||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||
coordinator.log_comms("supervisor connection attempt denied")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
|
||||
coordinator.log_comms("supervisor connection failed due to collision")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
coordinator.log_comms("supervisor connection failed due to version mismatch")
|
||||
else
|
||||
coordinator.log_comms("supervisor connection failed with no valid response")
|
||||
end
|
||||
end
|
||||
|
||||
ok = false
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
ok = false
|
||||
elseif (util.time_s() - self.est_last) > 1.0 then
|
||||
_send_establish()
|
||||
self.est_last = util.time_s()
|
||||
end
|
||||
elseif self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
self.est_tick_waiting = nil
|
||||
self.est_task_done = nil
|
||||
start_ui = true
|
||||
end
|
||||
|
||||
return ok, start_ui
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- attempt to connect to the subervisor
|
||||
---@nodiscard
|
||||
---@param timeout_s number timeout in seconds
|
||||
---@param tick_dmesg_waiting function callback to tick dmesg waiting
|
||||
---@param task_done function callback to show done on dmesg
|
||||
---@return boolean sv_linked true if connected, false otherwise
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
|
||||
local clock = util.new_clock(1)
|
||||
local start = util.time_s()
|
||||
local terminated = false
|
||||
|
||||
_send_establish()
|
||||
|
||||
clock.start()
|
||||
|
||||
while (util.time_s() - start) < timeout_s and (not self.sv_linked) and (not self.sv_config_err) do
|
||||
local event, p1, p2, p3, p4, p5 = util.pull_event()
|
||||
|
||||
if event == "timer" and clock.is_clock(p1) then
|
||||
-- timed out attempt, try again
|
||||
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
|
||||
_send_establish()
|
||||
clock.start()
|
||||
elseif event == "timer" then
|
||||
-- keep checking watchdog timers
|
||||
apisessions.check_all_watchdogs(p1)
|
||||
elseif event == "modem_message" then
|
||||
-- handle message
|
||||
local packet = public.parse_packet(p1, p2, p3, p4, p5)
|
||||
public.handle_packet(packet)
|
||||
elseif event == "terminate" then
|
||||
terminated = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
task_done(self.sv_linked)
|
||||
|
||||
if terminated then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
elseif not self.sv_linked then
|
||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||
coordinator.log_comms("supervisor connection attempt denied")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
|
||||
coordinator.log_comms("supervisor connection failed due to collision")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
coordinator.log_comms("supervisor connection failed due to version mismatch")
|
||||
else
|
||||
coordinator.log_comms("supervisor connection failed with no valid response")
|
||||
end
|
||||
end
|
||||
|
||||
return self.sv_linked
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMAND command
|
||||
function public.send_fac_command(cmd)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd })
|
||||
---@param option any? optional option options for the optional options (like waste mode)
|
||||
function public.send_fac_command(cmd, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
@@ -379,7 +385,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
-- send a unit command
|
||||
---@param cmd UNIT_COMMAND command
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?)
|
||||
---@param option any? optional option options for the optional options (like burn rate)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
@@ -424,7 +430,10 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|capi_frame|nil
|
||||
---@return boolean close_ui
|
||||
function public.handle_packet(packet)
|
||||
local was_linked = self.sv_linked
|
||||
|
||||
if packet ~= nil then
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local r_chan = packet.scada_frame.remote_channel()
|
||||
@@ -434,7 +443,9 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
if l_chan ~= crd_channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == pkt_channel then
|
||||
if protocol == PROTOCOL.COORD_API then
|
||||
if not self.sv_linked then
|
||||
log.debug("discarding pocket API packet before linked to supervisor")
|
||||
elseif protocol == PROTOCOL.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(src_addr)
|
||||
@@ -473,7 +484,6 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
elseif dev_type == DEVICE_TYPE.PKT then
|
||||
-- pocket linking request
|
||||
local id = apisessions.establish_session(src_addr, firmware_v)
|
||||
println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected"))
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
|
||||
@@ -496,12 +506,12 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
-- check sequence number
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.connected and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
return false
|
||||
elseif self.sv_linked and src_addr ~= self.sv_addr then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
|
||||
return
|
||||
return false
|
||||
else
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
@@ -563,6 +573,10 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
process.waste_ack_handle(packet.data[2])
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
process.pu_fb_ack_handle(packet.data[2])
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
@@ -627,70 +641,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
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 > 1 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = config[1], ---@type integer
|
||||
defs = {} -- boilers and turbines
|
||||
}
|
||||
|
||||
if (#config - 1) == (conf.num_units * 2) then
|
||||
-- record sequence of pairs of [#boilers, #turbines] per unit
|
||||
for i = 2, #config do
|
||||
table.insert(conf.defs, config[i])
|
||||
end
|
||||
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
self.sv_config_err = false
|
||||
else
|
||||
self.sv_config_err = true
|
||||
log.warning("invalid supervisor configuration definitions received, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("invalid supervisor configuration table received, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
elseif packet.length == 1 then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
log.warning("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
log.warning("supervisor comms version mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
elseif self.sv_linked then
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
@@ -715,11 +666,83 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
println_ts("server connection closed by remote host")
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
log.info("server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
if packet.length == 2 then
|
||||
local est_ack = packet.data[1]
|
||||
local config = packet.data[2]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
-- reset to disconnected before validating
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
|
||||
if type(config) == "table" and #config > 1 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = config[1], ---@type integer
|
||||
defs = {} -- boilers and turbines
|
||||
}
|
||||
|
||||
if (#config - 1) == (conf.num_units * 2) then
|
||||
-- record sequence of pairs of [#boilers, #turbines] per unit
|
||||
for i = 2, #config do
|
||||
table.insert(conf.defs, config[i])
|
||||
end
|
||||
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
self.sv_r_seq_num = nil
|
||||
self.sv_config_err = false
|
||||
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
|
||||
else
|
||||
self.sv_config_err = true
|
||||
log.warning("invalid supervisor configuration definitions received, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("invalid supervisor configuration table received, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
elseif packet.length == 1 then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
|
||||
log.warning("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
|
||||
log.warning("supervisor comms version mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet reply (len = 1) unsupported")
|
||||
end
|
||||
|
||||
self.last_est_ack = est_ack
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
||||
end
|
||||
@@ -730,6 +753,8 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
|
||||
log.debug("received packet for unknown channel " .. r_chan, true)
|
||||
end
|
||||
end
|
||||
|
||||
return was_linked and not self.sv_linked
|
||||
end
|
||||
|
||||
-- check if the coordinator is still linked to the supervisor
|
||||
|
||||
@@ -10,9 +10,15 @@ local util = require("scada-common.util")
|
||||
local process = require("coordinator.process")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local PROCESS = types.PROCESS
|
||||
|
||||
-- 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
|
||||
|
||||
local iocontrol = {}
|
||||
|
||||
---@class ioctl
|
||||
@@ -27,6 +33,19 @@ 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
|
||||
function iocontrol.init_fp(firmware_v, comms_v)
|
||||
---@class ioctl_front_panel
|
||||
io.fp = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
io.fp.ps.publish("version", firmware_v)
|
||||
io.fp.ps.publish("comms_version", comms_v)
|
||||
end
|
||||
|
||||
-- initialize the coordinator IO controller
|
||||
---@param conf facility_conf configuration
|
||||
---@param comms coord_comms comms reference
|
||||
@@ -52,6 +71,10 @@ function iocontrol.init(conf, comms)
|
||||
gen_fault = false
|
||||
},
|
||||
|
||||
---@type WASTE_PRODUCT
|
||||
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
auto_pu_fallback_active = false,
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
save_cfg_ack = __generic_ack,
|
||||
@@ -65,16 +88,21 @@ function iocontrol.init(conf, comms)
|
||||
induction_ps_tbl = {},
|
||||
induction_data_tbl = {},
|
||||
|
||||
sps_ps_tbl = {},
|
||||
sps_data_tbl = {},
|
||||
|
||||
tank_ps_tbl = {},
|
||||
tank_data_tbl = {},
|
||||
|
||||
env_d_ps = psil.create(),
|
||||
env_d_data = {}
|
||||
}
|
||||
|
||||
-- create induction tables (currently only 1 is supported)
|
||||
for _ = 1, conf.num_units do
|
||||
local data = {} ---@type imatrix_session_db
|
||||
table.insert(io.facility.induction_ps_tbl, psil.create())
|
||||
table.insert(io.facility.induction_data_tbl, data)
|
||||
end
|
||||
-- create induction and SPS tables (currently only 1 of each is supported)
|
||||
table.insert(io.facility.induction_ps_tbl, psil.create())
|
||||
table.insert(io.facility.induction_data_tbl, {})
|
||||
table.insert(io.facility.sps_ps_tbl, psil.create())
|
||||
table.insert(io.facility.sps_data_tbl, {})
|
||||
|
||||
io.units = {}
|
||||
for i = 1, conf.num_units do
|
||||
@@ -87,11 +115,15 @@ function iocontrol.init(conf, comms)
|
||||
|
||||
num_boilers = 0,
|
||||
num_turbines = 0,
|
||||
num_snas = 0,
|
||||
|
||||
control_state = false,
|
||||
burn_rate_cmd = 0.0,
|
||||
waste_control = 0,
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
sna_prod_rate = 0.0,
|
||||
|
||||
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
|
||||
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
|
||||
-- auto control group
|
||||
a_group = 0,
|
||||
@@ -100,10 +132,10 @@ function iocontrol.init(conf, comms)
|
||||
scram = function () process.scram(i) end,
|
||||
reset_rps = function () process.reset_rps(i) end,
|
||||
ack_alarms = function () process.ack_all_alarms(i) end,
|
||||
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
|
||||
set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode
|
||||
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
|
||||
set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode
|
||||
|
||||
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0
|
||||
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual
|
||||
|
||||
start_ack = __generic_ack,
|
||||
scram_ack = __generic_ack,
|
||||
@@ -152,7 +184,10 @@ function iocontrol.init(conf, comms)
|
||||
boiler_data_tbl = {},
|
||||
|
||||
turbine_ps_tbl = {},
|
||||
turbine_data_tbl = {}
|
||||
turbine_data_tbl = {},
|
||||
|
||||
tank_ps_tbl = {},
|
||||
tank_data_tbl = {}
|
||||
}
|
||||
|
||||
-- create boiler tables
|
||||
@@ -179,6 +214,92 @@ function iocontrol.init(conf, comms)
|
||||
process.init(io, comms)
|
||||
end
|
||||
|
||||
--#region Front Panel PSIL
|
||||
|
||||
-- toggle heartbeat indicator
|
||||
function iocontrol.heartbeat() io.fp.ps.toggle("heartbeat") end
|
||||
|
||||
-- report presence of the wireless modem
|
||||
---@param has_modem boolean
|
||||
function iocontrol.fp_has_modem(has_modem) io.fp.ps.publish("has_modem", has_modem) end
|
||||
|
||||
-- report presence of the speaker
|
||||
---@param has_speaker boolean
|
||||
function iocontrol.fp_has_speaker(has_speaker) io.fp.ps.publish("has_speaker", has_speaker) end
|
||||
|
||||
-- report supervisor link state
|
||||
---@param state integer
|
||||
function iocontrol.fp_link_state(state) io.fp.ps.publish("link_state", state) end
|
||||
|
||||
-- report monitor connection state
|
||||
---@param id integer unit ID or 0 for main
|
||||
function iocontrol.fp_monitor_state(id, connected)
|
||||
local name = "main_monitor"
|
||||
if id > 0 then name = "unit_monitor_" .. id end
|
||||
io.fp.ps.publish(name, connected)
|
||||
end
|
||||
|
||||
-- report PKT firmware version and PKT session connection state
|
||||
---@param session_id integer PKT session
|
||||
---@param fw string firmware version
|
||||
---@param s_addr integer PKT computer ID
|
||||
function iocontrol.fp_pkt_connected(session_id, fw, s_addr)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_fw", fw)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr))
|
||||
pgi.create_pkt_entry(session_id)
|
||||
end
|
||||
|
||||
-- report PKT session disconnected
|
||||
---@param session_id integer PKT session
|
||||
function iocontrol.fp_pkt_disconnected(session_id)
|
||||
pgi.delete_pkt_entry(session_id)
|
||||
end
|
||||
|
||||
-- transmit PKT session RTT
|
||||
---@param session_id integer PKT session
|
||||
---@param rtt integer round trip time
|
||||
function iocontrol.fp_pkt_rtt(session_id, rtt)
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt", rtt)
|
||||
|
||||
if rtt > HIGH_RTT then
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.red)
|
||||
elseif rtt > WARN_RTT then
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.yellow_hc)
|
||||
else
|
||||
io.fp.ps.publish("pkt_" .. session_id .. "_rtt_color", colors.green)
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Builds
|
||||
|
||||
-- record and publish multiblock RTU build data
|
||||
---@param id integer
|
||||
---@param entry table
|
||||
---@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)
|
||||
local exists = type(data_tbl[id]) == "table"
|
||||
if exists or create then
|
||||
if not exists then
|
||||
ps_tbl[id] = psil.create()
|
||||
data_tbl[id] = {}
|
||||
end
|
||||
|
||||
data_tbl[id].formed = entry[1] ---@type boolean
|
||||
data_tbl[id].build = entry[2] ---@type table
|
||||
|
||||
ps_tbl[id].publish("formed", entry[1])
|
||||
|
||||
for key, val in pairs(data_tbl[id].build) do ps_tbl[id].publish(key, val) end
|
||||
end
|
||||
|
||||
return exists or (create == true)
|
||||
end
|
||||
|
||||
-- populate facility structure builds
|
||||
---@param build table
|
||||
---@return boolean valid
|
||||
@@ -191,21 +312,29 @@ function iocontrol.record_facility_builds(build)
|
||||
-- induction matricies
|
||||
if type(build.induction) == "table" then
|
||||
for id, matrix in pairs(build.induction) do
|
||||
if type(fac.induction_data_tbl[id]) == "table" then
|
||||
fac.induction_data_tbl[id].formed = matrix[1] ---@type boolean
|
||||
fac.induction_data_tbl[id].build = matrix[2] ---@type table
|
||||
|
||||
fac.induction_ps_tbl[id].publish("formed", matrix[1])
|
||||
|
||||
for key, val in pairs(fac.induction_data_tbl[id].build) do
|
||||
fac.induction_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
else
|
||||
if not _record_multiblock_build(id, matrix, fac.induction_data_tbl, fac.induction_ps_tbl) then
|
||||
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- SPS
|
||||
if type(build.sps) == "table" then
|
||||
for id, sps in pairs(build.sps) do
|
||||
if not _record_multiblock_build(id, sps, fac.sps_data_tbl, fac.sps_ps_tbl) then
|
||||
log.debug(util.c("iocontrol.record_facility_builds: invalid SPS id ", id))
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- dynamic tanks
|
||||
if type(build.tanks) == "table" then
|
||||
for id, tank in pairs(build.tanks) do
|
||||
_record_multiblock_build(id, tank, fac.tank_data_tbl, fac.tank_ps_tbl, true)
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug("facility builds not a table")
|
||||
valid = false
|
||||
@@ -249,16 +378,7 @@ function iocontrol.record_unit_builds(builds)
|
||||
-- boiler builds
|
||||
if type(build.boilers) == "table" then
|
||||
for b_id, boiler in pairs(build.boilers) do
|
||||
if type(unit.boiler_data_tbl[b_id]) == "table" then
|
||||
unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
|
||||
unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
|
||||
|
||||
unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
|
||||
|
||||
for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
|
||||
unit.boiler_ps_tbl[b_id].publish(key, val)
|
||||
end
|
||||
else
|
||||
if not _record_multiblock_build(b_id, boiler, unit.boiler_data_tbl, unit.boiler_ps_tbl) then
|
||||
log.debug(util.c(log_header, "invalid boiler id ", b_id))
|
||||
valid = false
|
||||
end
|
||||
@@ -268,27 +388,49 @@ function iocontrol.record_unit_builds(builds)
|
||||
-- turbine builds
|
||||
if type(build.turbines) == "table" then
|
||||
for t_id, turbine in pairs(build.turbines) do
|
||||
if type(unit.turbine_data_tbl[t_id]) == "table" then
|
||||
unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
|
||||
unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
|
||||
|
||||
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
|
||||
|
||||
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
|
||||
unit.turbine_ps_tbl[t_id].publish(key, val)
|
||||
end
|
||||
else
|
||||
if not _record_multiblock_build(t_id, turbine, unit.turbine_data_tbl, unit.turbine_ps_tbl) then
|
||||
log.debug(util.c(log_header, "invalid turbine id ", t_id))
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- dynamic tank builds
|
||||
if type(build.tanks) == "table" then
|
||||
for d_id, d_tank in pairs(build.tanks) do
|
||||
_record_multiblock_build(d_id, d_tank, unit.tank_data_tbl, unit.tank_ps_tbl, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return valid
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Statuses
|
||||
|
||||
-- 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] ---@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)
|
||||
|
||||
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
|
||||
|
||||
return is_faulted
|
||||
end
|
||||
|
||||
-- update facility status
|
||||
---@param status table
|
||||
---@return boolean valid
|
||||
@@ -306,7 +448,7 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
local ctl_status = status[1]
|
||||
|
||||
if type(ctl_status) == "table" and #ctl_status == 14 then
|
||||
if type(ctl_status) == "table" and #ctl_status == 16 then
|
||||
fac.all_sys_ok = ctl_status[1]
|
||||
fac.auto_ready = ctl_status[2]
|
||||
|
||||
@@ -354,6 +496,12 @@ function iocontrol.update_facility_status(status)
|
||||
io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
|
||||
end
|
||||
end
|
||||
|
||||
fac.auto_current_waste_product = ctl_status[15]
|
||||
fac.auto_pu_fallback_active = ctl_status[16]
|
||||
|
||||
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
|
||||
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
|
||||
else
|
||||
log.debug(log_header .. "control status not a table or length mismatch")
|
||||
valid = false
|
||||
@@ -390,36 +538,23 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
for id, matrix in pairs(rtu_statuses.induction) do
|
||||
if type(fac.induction_data_tbl[id]) == "table" then
|
||||
local rtu_faulted = matrix[1] ---@type boolean
|
||||
fac.induction_data_tbl[id].formed = matrix[2] ---@type boolean
|
||||
fac.induction_data_tbl[id].state = matrix[3] ---@type table
|
||||
fac.induction_data_tbl[id].tanks = matrix[4] ---@type table
|
||||
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
|
||||
local ps = fac.induction_ps_tbl[id] ---@type psil
|
||||
|
||||
local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
|
||||
local rtu_faulted = _record_multiblock_status(matrix, data, ps)
|
||||
|
||||
fac.induction_ps_tbl[id].publish("formed", data.formed)
|
||||
fac.induction_ps_tbl[id].publish("faulted", rtu_faulted)
|
||||
|
||||
if data.formed then
|
||||
if rtu_faulted then
|
||||
fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
|
||||
elseif data.tanks.energy_fill >= 0.99 then
|
||||
fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
|
||||
if rtu_faulted then
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
elseif data.formed then
|
||||
if data.tanks.energy_fill >= 0.99 then
|
||||
ps.publish("computed_status", 6) -- full
|
||||
elseif data.tanks.energy_fill <= 0.01 then
|
||||
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
|
||||
ps.publish("computed_status", 5) -- empty
|
||||
else
|
||||
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
|
||||
ps.publish("computed_status", 4) -- on-line
|
||||
end
|
||||
else
|
||||
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed
|
||||
end
|
||||
|
||||
for key, val in pairs(fac.induction_data_tbl[id].state) do
|
||||
fac.induction_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(fac.induction_data_tbl[id].tanks) do
|
||||
fac.induction_ps_tbl[id].publish(key, val)
|
||||
ps.publish("computed_status", 2) -- not formed
|
||||
end
|
||||
else
|
||||
log.debug(util.c(log_header, "invalid induction matrix id ", id))
|
||||
@@ -430,6 +565,82 @@ function iocontrol.update_facility_status(status)
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- SPS statuses
|
||||
if type(rtu_statuses.sps) == "table" then
|
||||
for id = 1, #fac.sps_ps_tbl do
|
||||
if rtu_statuses.sps[id] == nil then
|
||||
-- disconnected
|
||||
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] ---@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
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
elseif data.formed then
|
||||
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
|
||||
log.debug(util.c(log_header, "invalid sps id ", id))
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "sps list not a table")
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- dynamic tank statuses
|
||||
if type(rtu_statuses.tanks) == "table" then
|
||||
for id = 1, #fac.tank_ps_tbl do
|
||||
if rtu_statuses.tanks[id] == nil then
|
||||
-- disconnected
|
||||
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] ---@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
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
elseif data.formed then
|
||||
if data.tanks.fill >= 0.99 then
|
||||
ps.publish("computed_status", 6) -- full
|
||||
elseif data.tanks.fill < 0.20 then
|
||||
ps.publish("computed_status", 5) -- low
|
||||
else
|
||||
ps.publish("computed_status", 4) -- on-line
|
||||
end
|
||||
else
|
||||
ps.publish("computed_status", 2) -- not formed
|
||||
end
|
||||
else
|
||||
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "dyanmic tank list not a table")
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- environment detector status
|
||||
if type(rtu_statuses.rad_mon) == "table" then
|
||||
if #rtu_statuses.rad_mon > 0 then
|
||||
@@ -472,6 +683,9 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
valid = false
|
||||
else
|
||||
local burn_rate_sum = 0.0
|
||||
local sna_count_sum = 0
|
||||
local pu_rate = 0.0
|
||||
local po_rate = 0.0
|
||||
|
||||
-- get all unit statuses
|
||||
for i = 1, #statuses do
|
||||
@@ -480,6 +694,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
local unit = io.units[i] ---@type ioctl_unit
|
||||
local status = statuses[i]
|
||||
|
||||
local burn_rate = 0.0
|
||||
|
||||
if type(status) ~= "table" or #status ~= 5 then
|
||||
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
|
||||
valid = false
|
||||
@@ -515,7 +731,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
-- if status hasn't been received, mek_status = {}
|
||||
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
|
||||
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
|
||||
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
|
||||
@@ -571,34 +788,21 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
for id, boiler in pairs(rtu_statuses.boilers) do
|
||||
if type(unit.boiler_data_tbl[id]) == "table" then
|
||||
local rtu_faulted = boiler[1] ---@type boolean
|
||||
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean
|
||||
unit.boiler_data_tbl[id].state = boiler[3] ---@type table
|
||||
unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
|
||||
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
||||
|
||||
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||
|
||||
unit.boiler_ps_tbl[id].publish("formed", data.formed)
|
||||
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
|
||||
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
|
||||
|
||||
if rtu_faulted then
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
elseif data.formed then
|
||||
if data.state.boil_rate > 0 then
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
|
||||
ps.publish("computed_status", 5) -- active
|
||||
else
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
|
||||
ps.publish("computed_status", 4) -- idle
|
||||
end
|
||||
else
|
||||
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.boiler_data_tbl[id].state) do
|
||||
unit.boiler_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
|
||||
unit.boiler_ps_tbl[id].publish(key, val)
|
||||
ps.publish("computed_status", 2) -- not formed
|
||||
end
|
||||
else
|
||||
log.debug(util.c(log_header, "invalid boiler id ", id))
|
||||
@@ -621,36 +825,23 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
|
||||
for id, turbine in pairs(rtu_statuses.turbines) do
|
||||
if type(unit.turbine_data_tbl[id]) == "table" then
|
||||
local rtu_faulted = turbine[1] ---@type boolean
|
||||
unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
|
||||
unit.turbine_data_tbl[id].state = turbine[3] ---@type table
|
||||
unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
|
||||
|
||||
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
||||
|
||||
unit.turbine_ps_tbl[id].publish("formed", data.formed)
|
||||
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
|
||||
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
|
||||
|
||||
if rtu_faulted then
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
elseif data.formed then
|
||||
if data.tanks.energy_fill >= 0.99 then
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
|
||||
ps.publish("computed_status", 6) -- trip
|
||||
elseif data.state.flow_rate < 100 then
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
|
||||
ps.publish("computed_status", 4) -- idle
|
||||
else
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
|
||||
ps.publish("computed_status", 5) -- active
|
||||
end
|
||||
else
|
||||
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.turbine_data_tbl[id].state) do
|
||||
unit.turbine_ps_tbl[id].publish(key, val)
|
||||
end
|
||||
|
||||
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
|
||||
unit.turbine_ps_tbl[id].publish(key, val)
|
||||
ps.publish("computed_status", 2) -- not formed
|
||||
end
|
||||
else
|
||||
log.debug(util.c(log_header, "invalid turbine id ", id))
|
||||
@@ -662,6 +853,58 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- dynamic tank statuses
|
||||
if type(rtu_statuses.tanks) == "table" then
|
||||
for id = 1, #unit.tank_ps_tbl do
|
||||
if rtu_statuses.tanks[i] == nil then
|
||||
-- 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] ---@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
|
||||
ps.publish("computed_status", 3) -- faulted
|
||||
elseif data.formed then
|
||||
if data.tanks.fill >= 0.99 then
|
||||
ps.publish("computed_status", 6) -- full
|
||||
elseif data.tanks.fill < 0.20 then
|
||||
ps.publish("computed_status", 5) -- low
|
||||
else
|
||||
ps.publish("computed_status", 5) -- active
|
||||
end
|
||||
else
|
||||
ps.publish("computed_status", 2) -- not formed
|
||||
end
|
||||
else
|
||||
log.debug(util.c(log_header, "invalid dynamic tank id ", id))
|
||||
valid = false
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "dynamic tank list not a table")
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- solar neutron activator status info
|
||||
if type(rtu_statuses.sna) == "table" then
|
||||
unit.num_snas = rtu_statuses.sna[1] ---@type integer
|
||||
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
|
||||
|
||||
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
|
||||
|
||||
sna_count_sum = sna_count_sum + unit.num_snas
|
||||
else
|
||||
log.debug(log_header .. "sna statistic list not a table")
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- environment detector status
|
||||
if type(rtu_statuses.rad_mon) == "table" then
|
||||
if #rtu_statuses.rad_mon > 0 then
|
||||
@@ -739,12 +982,17 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
local unit_state = status[5]
|
||||
|
||||
if type(unit_state) == "table" then
|
||||
if #unit_state == 5 then
|
||||
if #unit_state == 6 then
|
||||
unit.waste_mode = unit_state[5]
|
||||
unit.waste_product = unit_state[6]
|
||||
|
||||
unit.unit_ps.publish("U_StatusLine1", unit_state[1])
|
||||
unit.unit_ps.publish("U_StatusLine2", unit_state[2])
|
||||
unit.unit_ps.publish("U_WasteMode", unit_state[3])
|
||||
unit.unit_ps.publish("U_AutoReady", unit_state[4])
|
||||
unit.unit_ps.publish("U_AutoDegraded", unit_state[5])
|
||||
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)
|
||||
else
|
||||
log.debug(log_header .. "unit state length mismatch")
|
||||
valid = false
|
||||
@@ -753,10 +1001,18 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
log.debug(log_header .. "unit state not a table")
|
||||
valid = false
|
||||
end
|
||||
|
||||
-- determine waste production for this unit, add to statistics
|
||||
local is_pu = unit.waste_product == types.WASTE_PRODUCT.PLUTONIUM
|
||||
pu_rate = pu_rate + util.trinary(is_pu, burn_rate / 10.0, 0.0)
|
||||
po_rate = po_rate + util.trinary(not is_pu, math.min(burn_rate / 10.0, unit.sna_prod_rate), 0.0)
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
io.facility.ps.publish("po_rate", po_rate)
|
||||
|
||||
-- update alarm sounder
|
||||
sounder.eval(io.units)
|
||||
@@ -765,6 +1021,8 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
return valid
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- get the IO controller database
|
||||
function iocontrol.get_db() return io end
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
local PROCESS = types.PROCESS
|
||||
local PRODUCT = types.WASTE_PRODUCT
|
||||
|
||||
---@class process_controller
|
||||
local process = {}
|
||||
@@ -24,7 +25,9 @@ local self = {
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {}
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,19 +51,23 @@ function process.init(iocontrol, coord_comms)
|
||||
log.error("process.init(): failed to load coordinator settings file")
|
||||
end
|
||||
|
||||
-- facility auto control configuration
|
||||
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
|
||||
|
||||
if type(config) == "table" then
|
||||
self.config.mode = config.mode
|
||||
self.config.burn_target = config.burn_target
|
||||
self.config.charge_target = config.charge_target
|
||||
self.config.gen_target = config.gen_target
|
||||
self.config.limits = config.limits
|
||||
self.config.waste_product = config.waste_product
|
||||
self.config.pu_fallback = config.pu_fallback
|
||||
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", self.config.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback)
|
||||
|
||||
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
@@ -70,18 +77,18 @@ function process.init(iocontrol, coord_comms)
|
||||
log.info("PROCESS: loaded auto control settings from coord.settings")
|
||||
end
|
||||
|
||||
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
|
||||
|
||||
if type(waste_mode) == "table" then
|
||||
for id, mode in pairs(waste_mode) do
|
||||
-- unit waste states
|
||||
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil
|
||||
if type(waste_modes) == "table" then
|
||||
for id, mode in pairs(waste_modes) do
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded waste mode settings from coord.settings")
|
||||
log.info("PROCESS: loaded unit waste mode settings from coord.settings")
|
||||
end
|
||||
|
||||
-- unit priority groups
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||
@@ -137,7 +144,7 @@ end
|
||||
-- set waste mode
|
||||
---@param id integer unit ID
|
||||
---@param mode integer waste mode
|
||||
function process.set_waste(id, mode)
|
||||
function process.set_unit_waste(id, mode)
|
||||
-- publish so that if it fails then it gets reset
|
||||
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
|
||||
@@ -153,7 +160,7 @@ function process.set_waste(id, mode)
|
||||
settings.set("WASTE_MODES", waste_mode)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
log.error("process.set_waste(): failed to save coordinator settings file")
|
||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -204,6 +211,24 @@ end
|
||||
-- AUTO PROCESS CONTROL --
|
||||
--------------------------
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("process._write_auto_config(): failed to load coordinator settings file")
|
||||
end
|
||||
|
||||
-- save config
|
||||
settings.set("PROCESS", self.config)
|
||||
local saved = settings.save("/coord.settings")
|
||||
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return not not saved
|
||||
end
|
||||
|
||||
-- stop automatic process control
|
||||
function process.stop_auto()
|
||||
self.comms.send_fac_command(FAC_COMMAND.STOP)
|
||||
@@ -216,6 +241,30 @@ function process.start_auto()
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
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(FAC_COMMAND.SET_WASTE_MODE, product)
|
||||
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
|
||||
-- update config table and save
|
||||
self.config.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)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.config.pu_fallback = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- save process control settings
|
||||
---@param mode PROCESS control mode
|
||||
---@param burn_target number burn rate target
|
||||
@@ -223,29 +272,17 @@ end
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits table unit burn rate limits
|
||||
function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("process.save(): failed to load coordinator settings file")
|
||||
end
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- config table
|
||||
self.config = {
|
||||
mode = mode,
|
||||
burn_target = burn_target,
|
||||
charge_target = charge_target,
|
||||
gen_target = gen_target,
|
||||
limits = limits
|
||||
}
|
||||
-- update config table
|
||||
self.config.mode = mode
|
||||
self.config.burn_target = burn_target
|
||||
self.config.charge_target = charge_target
|
||||
self.config.gen_target = gen_target
|
||||
self.config.limits = limits
|
||||
|
||||
-- save config
|
||||
settings.set("PROCESS", self.config)
|
||||
local saved = settings.save("/coord.settings")
|
||||
|
||||
if not saved then
|
||||
log.warning("process.save(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
self.io.facility.save_cfg_ack(saved)
|
||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||
end
|
||||
|
||||
-- handle a start command acknowledgement
|
||||
@@ -258,16 +295,33 @@ function process.start_ack_handle(response)
|
||||
self.config.charge_target = response[4]
|
||||
self.config.gen_target = response[5]
|
||||
|
||||
for i = 1, #response[6] do
|
||||
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
||||
self.config.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", self.config.limits[i])
|
||||
end
|
||||
|
||||
self.io.facility.ps.publish("auto_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
end
|
||||
|
||||
-- record waste product state after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product state
|
||||
function process.waste_ack_handle(response)
|
||||
self.config.waste_product = response
|
||||
self.io.facility.ps.publish("process_waste_product", response)
|
||||
end
|
||||
|
||||
-- record plutonium fallback state after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback state
|
||||
function process.pu_fb_ack_handle(response)
|
||||
self.config.pu_fallback = response
|
||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||
end
|
||||
|
||||
return process
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
@@ -21,7 +25,9 @@ local engine = {
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
ui_ready = false,
|
||||
fp_ready = false,
|
||||
ui = {
|
||||
front_panel = nil, ---@type graphics_element|nil
|
||||
main_display = nil, ---@type graphics_element|nil
|
||||
unit_displays = {}
|
||||
}
|
||||
@@ -46,24 +52,10 @@ end
|
||||
---@param monitors monitors_struct
|
||||
function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
end
|
||||
|
||||
-- check if the renderer is configured to use a given monitor peripheral
|
||||
---@nodiscard
|
||||
---@param periph table peripheral
|
||||
---@return boolean is_used
|
||||
function renderer.is_monitor_used(periph)
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == periph then
|
||||
return true
|
||||
else
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
if monitor == periph then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
-- report to front panel as connected
|
||||
iocontrol.fp_monitor_state(0, true)
|
||||
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
|
||||
end
|
||||
|
||||
-- init all displays in use by the renderer
|
||||
@@ -75,6 +67,17 @@ function renderer.init_displays()
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
_init_display(monitor)
|
||||
end
|
||||
|
||||
-- init terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.fp.colors do
|
||||
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
|
||||
end
|
||||
end
|
||||
|
||||
-- check main display width
|
||||
@@ -109,6 +112,51 @@ function renderer.init_dmesg()
|
||||
log.direct_dmesg(engine.dmesg_window)
|
||||
end
|
||||
|
||||
-- start the coordinator front panel
|
||||
function renderer.start_fp()
|
||||
if not engine.fp_ready then
|
||||
-- show front panel view on terminal
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- report front panel as ready
|
||||
engine.fp_ready = true
|
||||
end
|
||||
end
|
||||
|
||||
-- close out the front panel
|
||||
function renderer.close_fp()
|
||||
if engine.fp_ready then
|
||||
if not engine.ui_ready then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
end
|
||||
|
||||
-- disable PGI
|
||||
pgi.unlink()
|
||||
|
||||
-- hide to stop animation callbacks and clear root UI elements
|
||||
engine.ui.front_panel.hide()
|
||||
engine.ui.front_panel = nil
|
||||
engine.fp_ready = false
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- start the coordinator GUI
|
||||
function renderer.start_ui()
|
||||
if not engine.ui_ready then
|
||||
@@ -116,13 +164,15 @@ function renderer.start_ui()
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
-- show main view on main monitor
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
if engine.monitors.primary ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for i = 1, #engine.monitors.unit_displays do
|
||||
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[i], i)
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
|
||||
-- start flasher callback task
|
||||
@@ -135,12 +185,14 @@ end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
if not engine.fp_ready then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
end
|
||||
|
||||
-- delete element trees
|
||||
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||
for _, display in ipairs(engine.ui.unit_displays) do display.delete() end
|
||||
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
|
||||
|
||||
-- report ui as not ready
|
||||
engine.ui_ready = false
|
||||
@@ -157,22 +209,121 @@ function renderer.close_ui()
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
|
||||
-- is the front panel ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.fp_ready() return engine.fp_ready end
|
||||
|
||||
-- is the UI ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a monitor peripheral being disconnected
|
||||
---@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
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == device then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.primary = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state(0, false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor peripheral being reconnected
|
||||
---@param name string monitor name
|
||||
---@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
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary_name == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.primary = device
|
||||
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
|
||||
|
||||
if engine.ui_ready and (engine.ui.main_display == nil) then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
else
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(0, true)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
|
||||
-- handle a touch event
|
||||
---@param event mouse_interaction|nil
|
||||
function renderer.handle_mouse(event)
|
||||
if engine.ui_ready and event ~= nil then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||
layout.handle_mouse(event)
|
||||
if event ~= nil then
|
||||
if engine.fp_ready and event.monitor == "terminal" then
|
||||
engine.ui.front_panel.handle_mouse(event)
|
||||
elseif engine.ui_ready then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||
layout.handle_mouse(event)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local config = require("coordinator.config")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
|
||||
local apisessions = {}
|
||||
|
||||
@@ -112,6 +113,7 @@ function apisessions.establish_session(source_addr, version)
|
||||
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
iocontrol.fp_pkt_connected(id, version, source_addr)
|
||||
log.debug(util.c("[API] established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = {}
|
||||
|
||||
@@ -9,8 +11,6 @@ local PROTOCOL = comms.PROTOCOL
|
||||
-- local CAPI_TYPE = comms.CAPI_TYPE
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
|
||||
local println = util.println
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
-- local RETRY_PERIOD = 1000
|
||||
@@ -69,6 +69,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
local function _close()
|
||||
self.conn_watchdog.cancel()
|
||||
self.connected = false
|
||||
iocontrol.fp_pkt_disconnected(id)
|
||||
end
|
||||
|
||||
-- send a CAPI packet
|
||||
@@ -140,6 +141,8 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
|
||||
|
||||
iocontrol.fp_pkt_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
@@ -172,7 +175,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
println("connection to pocket session " .. id .. " closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
@@ -211,7 +213,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("connection to pocket session " .. id .. " closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
@@ -21,7 +22,7 @@ local sounder = require("coordinator.sounder")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local COORDINATOR_VERSION = "v0.17.1"
|
||||
local COORDINATOR_VERSION = "v0.21.0"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -30,7 +31,6 @@ local log_graphics = coordinator.log_graphics
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_comms_connecting = coordinator.log_comms_connecting
|
||||
local log_crypto = coordinator.log_crypto
|
||||
|
||||
----------------------------------------
|
||||
@@ -80,6 +80,9 @@ local function main()
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
-- report versions/init fp PSIL
|
||||
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
||||
|
||||
-- setup monitors
|
||||
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
|
||||
if not configured or monitors == nil then
|
||||
@@ -127,6 +130,7 @@ local function main()
|
||||
sounder.init(speaker, config.SOUNDER_VOLUME)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
@@ -148,6 +152,7 @@ local function main()
|
||||
return
|
||||
else
|
||||
log_comms("wireless modem connected")
|
||||
iocontrol.fp_has_modem(true)
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
@@ -167,76 +172,54 @@ local function main()
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
----------------------------------------
|
||||
-- connect to the supervisor
|
||||
-- start front panel & UI start function
|
||||
----------------------------------------
|
||||
|
||||
-- attempt to connect to the supervisor or exit
|
||||
local function init_connect_sv()
|
||||
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_CHANNEL)
|
||||
log_graphics("starting front panel UI...")
|
||||
|
||||
-- attempt to establish a connection with the supervisory computer
|
||||
if not coord_comms.sv_connect(60, tick_waiting, task_done) then
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if not init_connect_sv() then
|
||||
println("startup> failed to connect to supervisor")
|
||||
log_sys("system shutdown")
|
||||
local fp_ok, fp_message = pcall(renderer.start_fp)
|
||||
if not fp_ok then
|
||||
renderer.close_fp()
|
||||
log_graphics(util.c("front panel UI error: ", fp_message))
|
||||
println_ts("front panel UI creation failed")
|
||||
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
|
||||
return
|
||||
else
|
||||
log_sys("supervisor connected, proceeding to UI start")
|
||||
end
|
||||
else log_graphics("front panel ready") end
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
----------------------------------------
|
||||
|
||||
-- start up the UI
|
||||
-- start up the main UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function init_start_ui()
|
||||
log_graphics("starting UI...")
|
||||
local function start_main_ui()
|
||||
log_graphics("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, message = pcall(renderer.start_ui)
|
||||
local ui_ok, ui_message = pcall(renderer.start_ui)
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
log_graphics(util.c("UI crashed: ", message))
|
||||
println_ts("UI crashed")
|
||||
log.fatal(util.c("GUI crashed with error ", message))
|
||||
log_graphics(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
|
||||
local ui_ok = init_start_ui()
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
----------------------------------------
|
||||
|
||||
local link_failed = false
|
||||
local ui_ok = true
|
||||
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
|
||||
if ui_ok then
|
||||
-- start connection watchdog
|
||||
conn_watchdog.feed()
|
||||
log.debug("startup> conn watchdog started")
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
log_sys("system started successfully")
|
||||
end
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- main event loop
|
||||
while ui_ok do
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
@@ -250,31 +233,32 @@ local function main()
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
println_ts("wireless modem disconnected!")
|
||||
|
||||
-- close out UI
|
||||
renderer.close_ui()
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.is_monitor_used(device) then
|
||||
---@todo will be handled properly in #249
|
||||
-- "halt and catch fire" style handling
|
||||
local msg = "lost a configured monitor, system will now exit"
|
||||
println_ts(msg)
|
||||
log_sys(msg)
|
||||
break
|
||||
if renderer.handle_disconnect(device) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost unused monitor, ignoring")
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
local msg = "lost alarm sounder speaker"
|
||||
println_ts(msg)
|
||||
log_sys(msg)
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
@@ -282,33 +266,50 @@ local function main()
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
nic.connect(device)
|
||||
|
||||
log_sys("comms modem reconnected")
|
||||
println_ts("wireless modem reconnected.")
|
||||
|
||||
-- re-init system
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
-- elseif type == "monitor" then
|
||||
---@todo will be handled properly in #249
|
||||
-- not supported, system will exit on loss of in-use monitors
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_reconnect(param1, device) then
|
||||
log_sys(util.c("configured monitor ", param1, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", param1, " connected"))
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
local msg = "alarm sounder speaker reconnected"
|
||||
println_ts(msg)
|
||||
log_sys(msg)
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
link_failed = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, proceeding to main UI start")
|
||||
ui_ok = start_main_ui()
|
||||
if not ui_ok then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
@@ -316,25 +317,19 @@ local function main()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- update date and time string for main display
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
if coord_comms.is_linked() then
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
local msg = "supervisor server timeout"
|
||||
log_comms(msg)
|
||||
println_ts(msg)
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, UI, and stop sounder
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
|
||||
if nic.connected() then
|
||||
-- try to re-connect to the supervisor
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
end
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
@@ -347,25 +342,19 @@ local function main()
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
coord_comms.handle_packet(packet)
|
||||
|
||||
-- check if it was a disconnect
|
||||
if not coord_comms.is_linked() then
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, UI, and stop sounder
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
|
||||
if nic.connected() then
|
||||
-- try to re-connect to the supervisor
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_touch" then
|
||||
-- handle a monitor touch event
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
@@ -374,10 +363,17 @@ local function main()
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
println_ts("terminate requested, closing connections...")
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
-- handle supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
else link_failed = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
@@ -386,15 +382,20 @@ local function main()
|
||||
end
|
||||
|
||||
renderer.close_ui()
|
||||
renderer.close_fp()
|
||||
sounder.stop()
|
||||
log_sys("system shutdown")
|
||||
|
||||
if link_failed then println_ts("failed to connect to supervisor") end
|
||||
if not ui_ok then println_ts("main UI creation failed") end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
end
|
||||
|
||||
if not xpcall(main, crash.handler) then
|
||||
pcall(renderer.close_ui)
|
||||
pcall(renderer.close_fp)
|
||||
pcall(sounder.stop)
|
||||
crash.exit()
|
||||
else
|
||||
|
||||
48
coordinator/ui/components/pkt_entry.lua
Normal file
48
coordinator/ui/components/pkt_entry.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
--
|
||||
-- Pocket Connection Entry
|
||||
--
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create a pocket list entry
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer PKT session ID
|
||||
local function init(parent, id)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
|
||||
|
||||
local ps_prefix = "pkt_" .. id .. "_"
|
||||
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
|
||||
|
||||
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
|
||||
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
|
||||
|
||||
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
|
||||
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -15,8 +15,10 @@ local TextBox = require("graphics.elements.textbox")
|
||||
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.hazard_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
@@ -43,7 +45,7 @@ local function new_view(root, x, y)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
local main = Div{parent=root,width=104,height=24,x=x,y=y}
|
||||
local main = Div{parent=root,width=128,height=24,x=x,y=y}
|
||||
|
||||
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
|
||||
@@ -52,12 +54,14 @@ local function new_view(root, x, y)
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
||||
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=cpair(colors.green,colors.gray)}
|
||||
|
||||
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
|
||||
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
|
||||
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end)
|
||||
|
||||
main.line_break()
|
||||
|
||||
@@ -99,7 +103,7 @@ local function new_view(root, x, y)
|
||||
-- process control --
|
||||
---------------------
|
||||
|
||||
local proc = Div{parent=main,width=78,height=24,x=27,y=1}
|
||||
local proc = Div{parent=main,width=103,height=24,x=27,y=1}
|
||||
|
||||
-----------------------------
|
||||
-- process control targets --
|
||||
@@ -148,46 +152,77 @@ local function new_view(root, x, y)
|
||||
|
||||
local rate_limits = {}
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for i = 1, 4 do
|
||||
local unit
|
||||
local tag_fg_bg = cpair(colors.gray,colors.white)
|
||||
local lim_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local ctl_fg = colors.lightGray
|
||||
local cur_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local cur_lu = colors.lightGray
|
||||
|
||||
if i <= facility.num_units then
|
||||
unit = units[i] ---@type ioctl_unit
|
||||
tag_fg_bg = cpair(colors.black,colors.lightBlue)
|
||||
lim_fg_bg = bw_fg_bg
|
||||
ctl_fg = colors.gray
|
||||
cur_fg_bg = cpair(colors.black,colors.brown)
|
||||
cur_lu = colors.black
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
|
||||
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
|
||||
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=lim_fg_bg}
|
||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
||||
|
||||
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 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}
|
||||
|
||||
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(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
|
||||
if i <= facility.num_units then
|
||||
rate_limits[i] = lim
|
||||
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)
|
||||
|
||||
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||
else
|
||||
lim.disable()
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- unit statuses --
|
||||
-------------------
|
||||
|
||||
local stat_div = Div{parent=proc,width=38,height=19,x=57,y=6}
|
||||
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for i = 1, 4 do
|
||||
local tag_fg_bg = cpair(colors.gray,colors.white)
|
||||
local ind_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
local ind_off = colors.lightGray
|
||||
|
||||
if i <= facility.num_units then
|
||||
tag_fg_bg = cpair(colors.black,colors.cyan)
|
||||
ind_fg_bg = bw_fg_bg
|
||||
ind_off = colors.gray
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
|
||||
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=12,height=4,fg_bg=bw_fg_bg}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
if i <= facility.num_units then
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
@@ -195,7 +230,7 @@ local function new_view(root, x, y)
|
||||
-------------------------
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.purple}
|
||||
|
||||
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||
|
||||
@@ -261,6 +296,60 @@ local function new_view(root, x, y)
|
||||
for i = 1, #rate_limits do rate_limits[i].enable() end
|
||||
end
|
||||
end)
|
||||
|
||||
------------------------------
|
||||
-- waste production control --
|
||||
------------------------------
|
||||
|
||||
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] ---@type ioctl_unit
|
||||
|
||||
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
|
||||
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=cpair(colors.white,colors.gray)}
|
||||
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)
|
||||
end
|
||||
|
||||
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=cpair(colors.black,colors.brown)}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=TEXT_ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
|
||||
|
||||
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
|
||||
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
|
||||
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.white,colors.black),radio_bg=colors.brown}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
|
||||
|
||||
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=cpair(colors.white,colors.gray)}
|
||||
|
||||
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
|
||||
|
||||
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
|
||||
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(facility.ps, "po_rate", po_rate.update)
|
||||
am_rate.register(facility.ps, "am_rate", am_rate.update)
|
||||
|
||||
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
|
||||
|
||||
sna_count.register(facility.ps, "sna_count", sna_count.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -33,41 +33,21 @@ local border = core.border
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
local waste_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)
|
||||
}
|
||||
}
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer
|
||||
local function init(parent, id)
|
||||
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
|
||||
local f_ps = iocontrol.get_db().facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
if unit == nil then return main end
|
||||
|
||||
local u_ps = unit.unit_ps
|
||||
local b_ps = unit.boiler_ps_tbl
|
||||
local t_ps = unit.turbine_ps_tbl
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
@@ -398,7 +378,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=waste_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)
|
||||
|
||||
|
||||
121
coordinator/ui/layout/front_panel.lua
Normal file
121
coordinator/ui/layout/front_panel.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
--
|
||||
-- Coordinator Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
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 TabBar = require("graphics.elements.controls.tabbar")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param num_units integer number of units (number of unit monitors)
|
||||
local function init(panel, num_units)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.header}
|
||||
|
||||
local page_div = Div{parent=panel,x=1,y=3}
|
||||
|
||||
--
|
||||
-- system indicators
|
||||
--
|
||||
|
||||
local main_page = Div{parent=page_div,x=1,y=1}
|
||||
|
||||
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
|
||||
|
||||
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
|
||||
status.update(true)
|
||||
system.line_break()
|
||||
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
system.line_break()
|
||||
|
||||
modem.register(ps, "has_modem", modem.update)
|
||||
network.register(ps, "link_state", network.update)
|
||||
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)}
|
||||
main_monitor.register(ps, "main_monitor", main_monitor.update)
|
||||
|
||||
monitors.line_break()
|
||||
|
||||
for i = 1, num_units do
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)}
|
||||
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
|
||||
end
|
||||
|
||||
--
|
||||
-- about footer
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
|
||||
|
||||
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)
|
||||
|
||||
--
|
||||
-- page handling
|
||||
--
|
||||
|
||||
-- API page
|
||||
|
||||
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- assemble page panes
|
||||
|
||||
local panes = { main_page, api_page }
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
|
||||
local tabs = {
|
||||
{ name = "CRD", color = cpair(colors.black, colors.ivory) },
|
||||
{ name = "API", color = cpair(colors.black, colors.ivory) },
|
||||
}
|
||||
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
|
||||
|
||||
-- link pocket API list management to PGI
|
||||
pgi.link_elements(api_list, pkt_entry)
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -9,7 +9,7 @@ local iocontrol = require("coordinator.iocontrol")
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local imatrix = require("coordinator.ui.components.imatrix")
|
||||
local process_ctl = require("coordinator.ui.components.processctl")
|
||||
local process_ctl = require("coordinator.ui.components.process_ctl")
|
||||
local unit_overview = require("coordinator.ui.components.unit_overview")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
58
coordinator/ui/pgi.lua
Normal file
58
coordinator/ui/pgi.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
--
|
||||
-- Protected Graphics Interface
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local pgi = {}
|
||||
|
||||
local data = {
|
||||
pkt_list = nil, ---@type nil|graphics_element
|
||||
pkt_entry = nil, ---@type function
|
||||
-- session entries
|
||||
s_entries = { pkt = {} }
|
||||
}
|
||||
|
||||
-- link list boxes
|
||||
---@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
|
||||
end
|
||||
|
||||
-- unlink all fields, disabling the PGI
|
||||
function pgi.unlink()
|
||||
data.pkt_list = nil
|
||||
data.pkt_entry = nil
|
||||
end
|
||||
|
||||
-- add a PKT entry to the PKT list
|
||||
---@param session_id integer pocket session
|
||||
function pgi.create_pkt_entry(session_id)
|
||||
if data.pkt_list ~= nil and data.pkt_entry ~= nil then
|
||||
local success, result = pcall(data.pkt_entry, data.pkt_list, session_id)
|
||||
|
||||
if success then
|
||||
data.s_entries.pkt[session_id] = result
|
||||
else
|
||||
log.error(util.c("PGI: failed to create PKT entry (", result, ")"), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- delete a PKT entry from the PKT list
|
||||
---@param session_id integer pocket session
|
||||
function pgi.delete_pkt_entry(session_id)
|
||||
if data.s_entries.pkt[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.pkt[session_id].delete)
|
||||
data.s_entries.pkt[session_id] = nil
|
||||
|
||||
if not success then
|
||||
log.error(util.c("PGI: failed to delete PKT entry (", result, ")"), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return pgi
|
||||
@@ -10,6 +10,41 @@ local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- add color mappings for front panel
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
-- front panel styling
|
||||
|
||||
style.fp = {}
|
||||
|
||||
style.fp.root = cpair(colors.black, colors.ivory)
|
||||
style.fp.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.fp.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
-- main GUI styling
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
@@ -151,7 +186,90 @@ style.imatrix = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "HIGH CHARGE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.sps = {
|
||||
-- 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.black, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.waste = {
|
||||
-- auto waste processing states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "PLUTONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "POLONIUM"
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "1.0.0"
|
||||
core.version = "1.0.1"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
@@ -20,6 +20,7 @@ local element = {}
|
||||
|
||||
---@alias graphics_args graphics_args_generic
|
||||
---|waiting_args
|
||||
---|checkbox_args
|
||||
---|hazard_button_args
|
||||
---|multi_button_args
|
||||
---|push_button_args
|
||||
|
||||
@@ -8,7 +8,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new color map
|
||||
|
||||
85
graphics/elements/controls/checkbox.lua
Normal file
85
graphics/elements/controls/checkbox.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- Checkbox Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class checkbox_args
|
||||
---@field label string checkbox text
|
||||
---@field box_fg_bg cpair colors for checkbox
|
||||
---@field callback function function to call on press
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new checkbox control
|
||||
---@param args checkbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function checkbox(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field")
|
||||
assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field")
|
||||
|
||||
args.height = 1
|
||||
args.width = 3 + string.len(args.label)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
e.window.setCursorPos(1, 1)
|
||||
|
||||
if e.value then
|
||||
-- show as selected
|
||||
e.window.setTextColor(args.box_fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.box_fg_bg.fgd)
|
||||
e.window.write("\x88")
|
||||
e.window.setTextColor(args.box_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.box_fg_bg.bkg)
|
||||
e.window.write("\x88")
|
||||
e.window.setTextColor(args.box_fg_bg.bkg)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
end
|
||||
|
||||
-- write label text
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write(args.label)
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return checkbox
|
||||
@@ -14,7 +14,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -16,7 +16,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -13,7 +13,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -16,7 +16,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -18,7 +18,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -6,7 +6,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
|
||||
-- new core map box
|
||||
---@nodiscard
|
||||
|
||||
@@ -14,7 +14,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -10,7 +10,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -16,7 +16,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -14,7 +14,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -15,7 +15,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer 1 if omitted, must be an odd number
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
@@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -15,7 +15,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -7,7 +7,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -11,7 +11,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new pipe network
|
||||
|
||||
@@ -11,7 +11,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -13,7 +13,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -11,7 +11,7 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
|
||||
@@ -43,8 +43,10 @@ end
|
||||
|
||||
-- start/resume the flasher periodic
|
||||
function flasher.run()
|
||||
active = true
|
||||
callback_250ms()
|
||||
if not active then
|
||||
active = true
|
||||
callback_250ms()
|
||||
end
|
||||
end
|
||||
|
||||
-- clear all blinking indicators and stop the flasher periodic
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -18,7 +18,7 @@ local coreio = require("pocket.coreio")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
|
||||
local POCKET_VERSION = "alpha-v0.5.1"
|
||||
local POCKET_VERSION = "alpha-v0.5.2"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -112,7 +112,7 @@ local function main()
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
println(util.c("UI error: ", message))
|
||||
log.error(util.c("startup> GUI crashed with error ", message))
|
||||
log.error(util.c("startup> GUI render failed with error ", message))
|
||||
else
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
-- Reactor PLC Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
@@ -28,7 +28,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
-- create new main view
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
local function init(panel)
|
||||
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
@@ -12,6 +12,7 @@ local cpair = core.cpair
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
@@ -28,7 +29,7 @@ style.colors = {
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
|
||||
@@ -929,47 +929,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
|
||||
---@cast packet mgmt_frame
|
||||
-- if linked, only accept packets from configured supervisor
|
||||
if self.linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- link request confirmation
|
||||
if packet.length == 1 then
|
||||
log.debug("received unsolicited establish response")
|
||||
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
self.status_cache = nil
|
||||
_send_struct()
|
||||
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
log.debug("re-sent initial status data due to re-establish")
|
||||
else
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
println_ts("received unsolicited link denial, unlinking")
|
||||
log.warning("unsolicited establish request denied")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
println_ts("received unsolicited link collision, unlinking")
|
||||
log.warning("unsolicited establish request collision")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
println_ts("received unsolicited link version mismatch, unlinking")
|
||||
log.warning("unsolicited establish request version mismatch")
|
||||
else
|
||||
println_ts("invalid unsolicited link response")
|
||||
log.debug("unsolicited unknown establish request response")
|
||||
end
|
||||
|
||||
-- unlink
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.linked = false
|
||||
end
|
||||
|
||||
-- clear this since this is for something that was unsolicited
|
||||
self.last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
|
||||
-- report link state
|
||||
databus.tx_link_state(est_ack + 1)
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 and type(packet.data[1]) == "number" then
|
||||
local timestamp = packet.data[1]
|
||||
|
||||
@@ -19,7 +19,7 @@ local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "v1.5.0"
|
||||
local R_PLC_VERSION = "v1.5.5"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -190,7 +190,7 @@ local function main()
|
||||
renderer.close_ui()
|
||||
println_ts(util.c("UI error: ", message))
|
||||
println("init> running without front panel")
|
||||
log.error(util.c("GUI crashed with error ", message))
|
||||
log.error(util.c("front panel GUI render failed with error ", message))
|
||||
log.info("init> running in headless mode without front panel")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +77,7 @@ function threads.thread__main(smem, init)
|
||||
loop_clock.start()
|
||||
|
||||
-- send updated data
|
||||
if nic.connected() then
|
||||
if nic.is_connected() then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
else
|
||||
@@ -116,7 +116,7 @@ function threads.thread__main(smem, init)
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or nic.connected() then
|
||||
if (not networked) or nic.is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
@@ -146,7 +146,7 @@ function threads.thread__main(smem, init)
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "modem_message" and networked and plc_state.init_ok and nic.connected() then
|
||||
elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then
|
||||
-- got a packet
|
||||
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
if packet ~= nil then
|
||||
@@ -177,16 +177,21 @@ function threads.thread__main(smem, init)
|
||||
nic.disconnect()
|
||||
|
||||
println_ts("comms modem disconnected!")
|
||||
log.error("comms modem disconnected")
|
||||
log.warning("comms modem disconnected")
|
||||
|
||||
plc_state.no_modem = true
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log.info("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
plc_state.no_modem = true
|
||||
plc_state.degraded = true
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- try to scram reactor if it is still connected
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
if plc_state.init_ok then
|
||||
-- try to scram reactor if it is still connected
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
end
|
||||
end
|
||||
|
||||
plc_state.degraded = true
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
@@ -230,7 +235,7 @@ function threads.thread__main(smem, init)
|
||||
rps.reset()
|
||||
end
|
||||
elseif networked and type == "modem" then
|
||||
if device.isWireless() then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
plc_dev.modem = device
|
||||
plc_state.no_modem = false
|
||||
@@ -244,6 +249,8 @@ function threads.thread__main(smem, init)
|
||||
if not plc_state.no_reactor then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log.info("wired modem reconnected")
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ local rtu = require("rtu.rtu")
|
||||
|
||||
local boilerv_rtu = {}
|
||||
|
||||
-- create new boiler (mek 10.1+) device
|
||||
-- create new boiler device
|
||||
---@nodiscard
|
||||
---@param boiler table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
|
||||
48
rtu/dev/dynamicv_rtu.lua
Normal file
48
rtu/dev/dynamicv_rtu.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local dynamicv_rtu = {}
|
||||
|
||||
-- create new dynamic tank device
|
||||
---@nodiscard
|
||||
---@param dynamic_tank table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
function dynamicv_rtu.new(dynamic_tank)
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- disable auto fault clearing
|
||||
dynamic_tank.__p_clear_fault()
|
||||
dynamic_tank.__p_disable_afc()
|
||||
|
||||
-- discrete inputs --
|
||||
unit.connect_di(dynamic_tank.isFormed)
|
||||
|
||||
-- coils --
|
||||
unit.connect_coil(function () dynamic_tank.incrementContainerEditMode() end, function () end)
|
||||
unit.connect_coil(function () dynamic_tank.decrementContainerEditMode() end, function () end)
|
||||
|
||||
-- input registers --
|
||||
-- multiblock properties
|
||||
unit.connect_input_reg(dynamic_tank.getLength)
|
||||
unit.connect_input_reg(dynamic_tank.getWidth)
|
||||
unit.connect_input_reg(dynamic_tank.getHeight)
|
||||
unit.connect_input_reg(dynamic_tank.getMinPos)
|
||||
unit.connect_input_reg(dynamic_tank.getMaxPos)
|
||||
-- build properties
|
||||
unit.connect_input_reg(dynamic_tank.getTankCapacity)
|
||||
unit.connect_input_reg(dynamic_tank.getChemicalTankCapacity)
|
||||
-- tanks/containers
|
||||
unit.connect_input_reg(dynamic_tank.getStored)
|
||||
unit.connect_input_reg(dynamic_tank.getFilledPercentage)
|
||||
|
||||
-- holding registers --
|
||||
unit.connect_holding_reg(dynamic_tank.getContainerEditMode, dynamic_tank.setContainerEditMode)
|
||||
|
||||
-- check if any calls faulted
|
||||
local faulted = dynamic_tank.__p_is_faulted()
|
||||
dynamic_tank.__p_clear_fault()
|
||||
dynamic_tank.__p_enable_afc()
|
||||
|
||||
return unit.interface(), faulted
|
||||
end
|
||||
|
||||
return dynamicv_rtu
|
||||
@@ -2,7 +2,7 @@ local rtu = require("rtu.rtu")
|
||||
|
||||
local turbinev_rtu = {}
|
||||
|
||||
-- create new turbine (mek 10.1+) device
|
||||
-- create new turbine device
|
||||
---@nodiscard
|
||||
---@param turbine table
|
||||
---@return rtu_device interface, boolean faulted
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
-- RTU Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
@@ -26,6 +26,7 @@ local UNIT_TYPE_LABELS = {
|
||||
"REDSTONE",
|
||||
"BOILER",
|
||||
"TURBINE",
|
||||
"DYNAMIC TANK",
|
||||
"IND MATRIX",
|
||||
"SPS",
|
||||
"SNA",
|
||||
@@ -33,7 +34,7 @@ local UNIT_TYPE_LABELS = {
|
||||
}
|
||||
|
||||
|
||||
-- create new main view
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param units table unit list
|
||||
local function init(panel, units)
|
||||
|
||||
@@ -12,6 +12,7 @@ local cpair = core.cpair
|
||||
|
||||
-- remap global colors
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
@@ -28,7 +29,7 @@ style.colors = {
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
|
||||
@@ -22,6 +22,7 @@ local rtu = require("rtu.rtu")
|
||||
local threads = require("rtu.threads")
|
||||
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local redstone_rtu = require("rtu.dev.redstone_rtu")
|
||||
@@ -29,7 +30,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "v1.4.0"
|
||||
local RTU_VERSION = "v1.5.4"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
@@ -342,6 +343,18 @@ local function main()
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "dynamicValve" then
|
||||
-- dynamic tank multiblock
|
||||
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||
rtu_iface, faulted = dynamicv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
|
||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
||||
return false
|
||||
end
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock
|
||||
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
||||
@@ -464,7 +477,7 @@ local function main()
|
||||
renderer.close_ui()
|
||||
println_ts(util.c("UI error: ", message))
|
||||
println("startup> running without front panel")
|
||||
log.error(util.c("GUI crashed with error ", message))
|
||||
log.error(util.c("front panel GUI render failed with error ", message))
|
||||
log.info("startup> running in headless mode without front panel")
|
||||
end
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ local modbus = require("rtu.modbus")
|
||||
local renderer = require("rtu.renderer")
|
||||
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
@@ -97,9 +98,15 @@ function threads.thread__main(smem)
|
||||
nic.disconnect()
|
||||
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.warning("comms modem disconnected!")
|
||||
log.warning("comms modem disconnected")
|
||||
|
||||
databus.tx_hw_modem(false)
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log.info("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
databus.tx_hw_modem(false)
|
||||
end
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
@@ -127,7 +134,7 @@ function threads.thread__main(smem)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
nic.connect(device)
|
||||
|
||||
@@ -135,6 +142,8 @@ function threads.thread__main(smem)
|
||||
log.info("comms modem reconnected")
|
||||
|
||||
databus.tx_hw_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log.info("wired modem reconnected")
|
||||
end
|
||||
@@ -181,21 +190,22 @@ function threads.thread__main(smem)
|
||||
databus.tx_unit_hw_type(unit.uid, unit.type)
|
||||
end
|
||||
|
||||
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
|
||||
|
||||
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
unit.rtu, faulted = boilerv_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
unit.rtu, faulted = turbinev_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
unit.rtu, faulted = dynamicv_rtu.new(device)
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
unit.rtu, faulted = imatrix_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
||||
unit.rtu, faulted = sps_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(faulted, false, nil)
|
||||
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
||||
unit.rtu, faulted = sna_rtu.new(device)
|
||||
@@ -441,6 +451,12 @@ function threads.thread__unit_comms(smem, unit)
|
||||
unit.rtu, faulted = turbinev_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "dynamicValve" and unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
-- dynamic tank multiblock
|
||||
unit.device = device
|
||||
unit.rtu, faulted = dynamicv_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
-- induction matrix multiblock
|
||||
unit.device = device
|
||||
|
||||
@@ -14,7 +14,7 @@ local max_distance = nil ---@type number|nil maximum acceptable t
|
||||
---@class comms
|
||||
local comms = {}
|
||||
|
||||
comms.version = "2.1.0"
|
||||
comms.version = "2.1.2"
|
||||
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
@@ -92,9 +92,11 @@ local PLC_AUTO_ACK = {
|
||||
---@enum FAC_COMMAND
|
||||
local FAC_COMMAND = {
|
||||
SCRAM_ALL = 0, -- SCRAM all reactors
|
||||
STOP = 1, -- stop automatic control
|
||||
START = 2, -- start automatic control
|
||||
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units
|
||||
STOP = 1, -- stop automatic process control
|
||||
START = 2, -- start automatic process control
|
||||
ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
|
||||
SET_WASTE_MODE = 4, -- set automatic waste processing mode
|
||||
SET_PU_FB = 5 -- set plutonium fallback mode
|
||||
}
|
||||
|
||||
---@enum UNIT_COMMAND
|
||||
|
||||
@@ -20,7 +20,9 @@ local logger = {
|
||||
mode = MODE.APPEND,
|
||||
debug = false,
|
||||
file = nil,
|
||||
dmesg_out = nil
|
||||
dmesg_out = nil,
|
||||
dmesg_restore_coord = { 1, 1 },
|
||||
dmesg_scroll_count = 0
|
||||
}
|
||||
|
||||
---@type function
|
||||
@@ -158,6 +160,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
@@ -193,6 +196,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
@@ -201,6 +205,8 @@ function log.dmesg(msg, tag, tag_color)
|
||||
out.write(lines[i])
|
||||
end
|
||||
|
||||
logger.dmesg_restore_coord = { out.getCursorPos() }
|
||||
|
||||
_log(util.c("[", t_stamp, "] [", tag, "] ", msg))
|
||||
end
|
||||
|
||||
@@ -215,6 +221,7 @@ end
|
||||
---@return function update, function done
|
||||
function log.dmesg_working(msg, tag, tag_color)
|
||||
local ts_coord = log.dmesg(msg, tag, tag_color)
|
||||
local initial_scroll = logger.dmesg_scroll_count
|
||||
|
||||
local out = logger.dmesg_out
|
||||
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
||||
@@ -225,11 +232,14 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
local counter = 0
|
||||
|
||||
local function update(sec_remaining)
|
||||
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
||||
if new_y < 1 then return end
|
||||
|
||||
local time = util.sprintf("%ds", sec_remaining)
|
||||
local available = width - (string.len(time) + 2)
|
||||
local progress = ""
|
||||
|
||||
out.setCursorPos(ts_coord.x1, ts_coord.y)
|
||||
out.setCursorPos(ts_coord.x1, new_y)
|
||||
out.write(" ")
|
||||
|
||||
if counter % 4 == 0 then
|
||||
@@ -249,10 +259,15 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
out.setTextColor(initial_color)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
||||
end
|
||||
|
||||
local function done(ok)
|
||||
out.setCursorPos(ts_coord.x1, ts_coord.y)
|
||||
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
||||
if new_y < 1 then return end
|
||||
|
||||
out.setCursorPos(ts_coord.x1, new_y)
|
||||
|
||||
if ok or ok == nil then
|
||||
out.setTextColor(colors.green)
|
||||
@@ -263,6 +278,8 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
end
|
||||
|
||||
out.setTextColor(initial_color)
|
||||
|
||||
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
||||
end
|
||||
|
||||
return update, done
|
||||
|
||||
@@ -94,7 +94,7 @@ function network.nic(modem)
|
||||
|
||||
-- check if this NIC has a connected modem
|
||||
---@nodiscard
|
||||
function public.connected() return self.connected end
|
||||
function public.is_connected() return self.connected end
|
||||
|
||||
-- connect to a modem peripheral
|
||||
---@param reconnected_modem table
|
||||
|
||||
@@ -89,16 +89,18 @@ types.RTU_UNIT_TYPE = {
|
||||
REDSTONE = 1, -- redstone I/O
|
||||
BOILER_VALVE = 2, -- boiler mekanism 10.1+
|
||||
TURBINE_VALVE = 3, -- turbine, mekanism 10.1+
|
||||
IMATRIX = 4, -- induction matrix
|
||||
SPS = 5, -- SPS
|
||||
SNA = 6, -- SNA
|
||||
ENV_DETECTOR = 7 -- environment detector
|
||||
DYNAMIC_VALVE = 4, -- dynamic tank, mekanism 10.1+
|
||||
IMATRIX = 5, -- induction matrix
|
||||
SPS = 6, -- SPS
|
||||
SNA = 7, -- SNA
|
||||
ENV_DETECTOR = 8 -- environment detector
|
||||
}
|
||||
|
||||
types.RTU_UNIT_NAMES = {
|
||||
"redstone",
|
||||
"boiler_valve",
|
||||
"turbine_valve",
|
||||
"dynamic_valve",
|
||||
"induction_matrix",
|
||||
"sps",
|
||||
"sna",
|
||||
@@ -115,6 +117,7 @@ function types.rtu_type_to_string(utype)
|
||||
elseif utype == types.RTU_UNIT_TYPE.REDSTONE or
|
||||
utype == types.RTU_UNIT_TYPE.BOILER_VALVE or
|
||||
utype == types.RTU_UNIT_TYPE.TURBINE_VALVE or
|
||||
utype == types.RTU_UNIT_TYPE.DYNAMIC_VALVE or
|
||||
utype == types.RTU_UNIT_TYPE.IMATRIX or
|
||||
utype == types.RTU_UNIT_TYPE.SPS or
|
||||
utype == types.RTU_UNIT_TYPE.SNA or
|
||||
@@ -158,13 +161,26 @@ types.PROCESS_NAMES = {
|
||||
---@enum WASTE_MODE
|
||||
types.WASTE_MODE = {
|
||||
AUTO = 1,
|
||||
PLUTONIUM = 2,
|
||||
POLONIUM = 3,
|
||||
ANTI_MATTER = 4
|
||||
MANUAL_PLUTONIUM = 2,
|
||||
MANUAL_POLONIUM = 3,
|
||||
MANUAL_ANTI_MATTER = 4
|
||||
}
|
||||
|
||||
types.WASTE_MODE_NAMES = {
|
||||
"AUTO",
|
||||
"MANUAL_PLUTONIUM",
|
||||
"MANUAL_POLONIUM",
|
||||
"MANUAL_ANTI_MATTER"
|
||||
}
|
||||
|
||||
---@enum WASTE_PRODUCT
|
||||
types.WASTE_PRODUCT = {
|
||||
PLUTONIUM = 1,
|
||||
POLONIUM = 2,
|
||||
ANTI_MATTER = 3
|
||||
}
|
||||
|
||||
types.WASTE_PRODUCT_NAMES = {
|
||||
"PLUTONIUM",
|
||||
"POLONIUM",
|
||||
"ANTI_MATTER"
|
||||
@@ -315,6 +331,17 @@ types.RPS_TRIP_CAUSE = {
|
||||
FORCE_DISABLED = "force_disabled"
|
||||
}
|
||||
|
||||
---@alias container_mode
|
||||
---| "BOTH"
|
||||
---| "FILL"
|
||||
---| "EMPTY"
|
||||
|
||||
types.CONTAINER_MODE = {
|
||||
BOTH = "BOTH",
|
||||
FILL = "FILL",
|
||||
EMPTY = "EMPTY"
|
||||
}
|
||||
|
||||
---@alias dumping_mode
|
||||
---| "IDLE"
|
||||
---| "DUMPING"
|
||||
|
||||
@@ -11,6 +11,9 @@ local rsctl = require("supervisor.session.rsctl")
|
||||
local PROCESS = types.PROCESS
|
||||
local PROCESS_NAMES = types.PROCESS_NAMES
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
|
||||
local IO = rsio.IO
|
||||
|
||||
@@ -59,8 +62,11 @@ function facility.new(num_reactors, cooling_conf)
|
||||
all_sys_ok = false,
|
||||
-- rtus
|
||||
rtu_conn_count = 0,
|
||||
rtu_list = {},
|
||||
redstone = {},
|
||||
induction = {},
|
||||
sps = {},
|
||||
tanks = {},
|
||||
envd = {},
|
||||
-- redstone I/O control
|
||||
io_ctl = nil, ---@type rs_controller
|
||||
@@ -99,6 +105,10 @@ function facility.new(num_reactors, cooling_conf)
|
||||
last_update = 0,
|
||||
last_error = 0.0,
|
||||
last_time = 0.0,
|
||||
-- waste processing
|
||||
waste_product = WASTE.PLUTONIUM,
|
||||
current_waste_product = WASTE.PLUTONIUM,
|
||||
pu_fallback = false,
|
||||
-- statistics
|
||||
im_stat_init = false,
|
||||
avg_charge = util.mov_avg(3, 0.0),
|
||||
@@ -112,15 +122,12 @@ function facility.new(num_reactors, cooling_conf)
|
||||
table.insert(self.group_map, 0)
|
||||
end
|
||||
|
||||
-- list for RTU session management
|
||||
self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd }
|
||||
|
||||
-- init redstone RTU I/O controller
|
||||
self.io_ctl = rsctl.new(self.redstone)
|
||||
|
||||
-- unlink disconnected units
|
||||
---@param sessions table
|
||||
local function _unlink_disconnected_units(sessions)
|
||||
util.filter_table(sessions, function (u) return u.is_connected() end)
|
||||
end
|
||||
|
||||
-- check if all auto-controlled units completed ramping
|
||||
---@nodiscard
|
||||
local function _all_units_ramped()
|
||||
@@ -205,24 +212,50 @@ function facility.new(num_reactors, cooling_conf)
|
||||
table.insert(self.redstone, rs_unit)
|
||||
end
|
||||
|
||||
-- link an imatrix RTU session
|
||||
-- link an induction matrix RTU session
|
||||
---@param imatrix unit_session
|
||||
---@return boolean linked induction matrix accepted (max 1)
|
||||
function public.add_imatrix(imatrix)
|
||||
table.insert(self.induction, imatrix)
|
||||
if #self.induction == 0 then
|
||||
table.insert(self.induction, imatrix)
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- link an SPS RTU session
|
||||
---@param sps unit_session
|
||||
---@return boolean linked SPS accepted (max 1)
|
||||
function public.add_sps(sps)
|
||||
if #self.sps == 0 then
|
||||
table.insert(self.sps, sps)
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- link a dynamic tank RTU session
|
||||
---@param dynamic_tank unit_session
|
||||
---@return boolean linked dynamic tank accepted (max 1)
|
||||
function public.add_tank(dynamic_tank)
|
||||
if #self.tanks == 0 then
|
||||
table.insert(self.tanks, dynamic_tank)
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- link an environment detector RTU session
|
||||
---@param envd unit_session
|
||||
---@return boolean linked environment detector accepted (max 1)
|
||||
function public.add_envd(envd)
|
||||
table.insert(self.envd, envd)
|
||||
if #self.envd == 0 then
|
||||
table.insert(self.envd, envd)
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- purge devices associated with the given RTU session ID
|
||||
---@param session integer RTU session ID
|
||||
function public.purge_rtu_devices(session)
|
||||
util.filter_table(self.redstone, function (s) return s.get_session_id() ~= session end)
|
||||
util.filter_table(self.induction, function (s) return s.get_session_id() ~= session end)
|
||||
util.filter_table(self.envd, function (s) return s.get_session_id() ~= session end)
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end
|
||||
end
|
||||
|
||||
-- UPDATE --
|
||||
@@ -236,9 +269,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
-- update (iterate) the facility management
|
||||
function public.update()
|
||||
-- unlink RTU unit sessions if they are closed
|
||||
_unlink_disconnected_units(self.redstone)
|
||||
_unlink_disconnected_units(self.induction)
|
||||
_unlink_disconnected_units(self.envd)
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
||||
|
||||
-- current state for process control
|
||||
local charge_update = 0
|
||||
@@ -277,6 +308,8 @@ function facility.new(num_reactors, cooling_conf)
|
||||
-- Run Process Control --
|
||||
-------------------------
|
||||
|
||||
--#region Process Control
|
||||
|
||||
local avg_charge = self.avg_charge.compute()
|
||||
local avg_inflow = self.avg_inflow.compute()
|
||||
|
||||
@@ -542,10 +575,14 @@ function facility.new(num_reactors, cooling_conf)
|
||||
next_mode = PROCESS.INACTIVE
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
------------------------------
|
||||
-- Evaluate Automatic SCRAM --
|
||||
------------------------------
|
||||
|
||||
--#region Automatic SCRAM
|
||||
|
||||
local astatus = self.ascram_status
|
||||
|
||||
if self.induction[1] ~= nil then
|
||||
@@ -659,6 +696,8 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- update last mode and set next mode
|
||||
self.last_mode = self.mode
|
||||
self.mode = next_mode
|
||||
@@ -692,12 +731,33 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
self.io_ctl.digital_write(IO.F_ALARM, has_alarm)
|
||||
end
|
||||
|
||||
-----------------------------
|
||||
-- Update Waste Processing --
|
||||
-----------------------------
|
||||
|
||||
local insufficent_po_rate = false
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
if u.get_control_inf().waste_mode == WASTE_MODE.AUTO then
|
||||
if (u.get_sna_rate() * 10.0) < u.get_burn_rate() then
|
||||
insufficent_po_rate = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
|
||||
self.current_waste_product = WASTE.PLUTONIUM
|
||||
else self.current_waste_product = self.waste_product end
|
||||
end
|
||||
|
||||
-- call the update function of all units in the facility
|
||||
-- call the update function of all units in the facility<br>
|
||||
-- additionally sets the requested auto waste mode if applicable
|
||||
function public.update_units()
|
||||
for i = 1, #self.units do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
u.auto_set_waste(self.current_waste_product)
|
||||
u.update()
|
||||
end
|
||||
end
|
||||
@@ -721,15 +781,15 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
|
||||
-- stop auto control
|
||||
function public.auto_stop()
|
||||
self.mode = PROCESS.INACTIVE
|
||||
end
|
||||
function public.auto_stop() self.mode = PROCESS.INACTIVE end
|
||||
|
||||
-- set automatic control configuration and start the process
|
||||
---@param config coord_auto_config configuration
|
||||
---@return table response ready state (successfully started) and current configuration (after updating)
|
||||
function public.auto_start(config)
|
||||
local ready = false
|
||||
local charge_scaler = 1000000 -- convert MFE to FE
|
||||
local gen_scaler = 1000 -- convert kFE to FE
|
||||
local ready = false
|
||||
|
||||
-- load up current limits
|
||||
local limits = {}
|
||||
@@ -749,11 +809,11 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
|
||||
if (type(config.charge_target) == "number") and config.charge_target >= 0 then
|
||||
self.charge_setpoint = config.charge_target * 1000000 -- convert MFE to FE
|
||||
self.charge_setpoint = config.charge_target * charge_scaler
|
||||
end
|
||||
|
||||
if (type(config.gen_target) == "number") and config.gen_target >= 0 then
|
||||
self.gen_rate_setpoint = config.gen_target * 1000 -- convert kFE to FE
|
||||
self.gen_rate_setpoint = config.gen_target * gen_scaler
|
||||
end
|
||||
|
||||
if (type(config.limits) == "table") and (#config.limits == num_reactors) then
|
||||
@@ -769,11 +829,9 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
ready = self.mode_set > 0
|
||||
|
||||
if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) then
|
||||
ready = false
|
||||
elseif (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) then
|
||||
ready = false
|
||||
elseif (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then
|
||||
if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) or
|
||||
(self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) or
|
||||
(self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then
|
||||
ready = false
|
||||
end
|
||||
|
||||
@@ -782,7 +840,14 @@ function facility.new(num_reactors, cooling_conf)
|
||||
if ready then self.mode = self.mode_set end
|
||||
end
|
||||
|
||||
return { ready, self.mode_set, self.burn_target, self.charge_setpoint, self.gen_rate_setpoint, limits }
|
||||
return {
|
||||
ready,
|
||||
self.mode_set,
|
||||
self.burn_target,
|
||||
self.charge_setpoint / charge_scaler,
|
||||
self.gen_rate_setpoint / gen_scaler,
|
||||
limits
|
||||
}
|
||||
end
|
||||
|
||||
-- SETTINGS --
|
||||
@@ -807,15 +872,35 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
end
|
||||
|
||||
-- set waste production
|
||||
---@param product WASTE_PRODUCT target product
|
||||
---@return WASTE_PRODUCT product newly set value, if valid
|
||||
function public.set_waste_product(product)
|
||||
if product == WASTE.PLUTONIUM or product == WASTE.POLONIUM or product == WASTE.ANTI_MATTER then
|
||||
self.waste_product = product
|
||||
end
|
||||
|
||||
return self.waste_product
|
||||
end
|
||||
|
||||
-- enable/disable plutonium fallback
|
||||
---@param enabled boolean requested state
|
||||
---@return boolean enabled newly set value
|
||||
function public.set_pu_fallback(enabled)
|
||||
self.pu_fallback = enabled == true
|
||||
return self.pu_fallback
|
||||
end
|
||||
|
||||
-- READ STATES/PROPERTIES --
|
||||
|
||||
-- get build properties of all machines
|
||||
-- get build properties of all facility devices
|
||||
---@nodiscard
|
||||
---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude
|
||||
function public.get_build(inc_imatrix)
|
||||
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
|
||||
function public.get_build(type)
|
||||
local all = type == nil
|
||||
local build = {}
|
||||
|
||||
if inc_imatrix ~= false then
|
||||
if all or type == RTU_UNIT_TYPE.IMATRIX then
|
||||
build.induction = {}
|
||||
for i = 1, #self.induction do
|
||||
local matrix = self.induction[i] ---@type unit_session
|
||||
@@ -823,6 +908,22 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
end
|
||||
|
||||
if all or type == RTU_UNIT_TYPE.SPS then
|
||||
build.sps = {}
|
||||
for i = 1, #self.sps do
|
||||
local sps = self.sps[i] ---@type unit_session
|
||||
build.sps[sps.get_device_idx()] = { sps.get_db().formed, sps.get_db().build }
|
||||
end
|
||||
end
|
||||
|
||||
if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
build.tanks = {}
|
||||
for i = 1, #self.tanks do
|
||||
local tank = self.tanks[i] ---@type unit_session
|
||||
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
|
||||
end
|
||||
end
|
||||
|
||||
return build
|
||||
end
|
||||
|
||||
@@ -844,7 +945,9 @@ function facility.new(num_reactors, cooling_conf)
|
||||
astat.gen_fault or self.mode == PROCESS.GEN_RATE_FAULT_IDLE,
|
||||
self.status_text[1],
|
||||
self.status_text[2],
|
||||
self.group_map
|
||||
self.group_map,
|
||||
self.current_waste_product,
|
||||
(self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -866,23 +969,32 @@ function facility.new(num_reactors, cooling_conf)
|
||||
-- status of induction matricies (including tanks)
|
||||
status.induction = {}
|
||||
for i = 1, #self.induction do
|
||||
local matrix = self.induction[i] ---@type unit_session
|
||||
status.induction[matrix.get_device_idx()] = {
|
||||
matrix.is_faulted(),
|
||||
matrix.get_db().formed,
|
||||
matrix.get_db().state,
|
||||
matrix.get_db().tanks
|
||||
}
|
||||
local matrix = self.induction[i] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
status.induction[matrix.get_device_idx()] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
|
||||
end
|
||||
|
||||
-- status of sps
|
||||
status.sps = {}
|
||||
for i = 1, #self.sps do
|
||||
local sps = self.sps[i] ---@type unit_session
|
||||
local db = sps.get_db() ---@type sps_session_db
|
||||
status.sps[sps.get_device_idx()] = { sps.is_faulted(), db.formed, db.state, db.tanks }
|
||||
end
|
||||
|
||||
-- status of dynamic tanks
|
||||
status.tanks = {}
|
||||
for i = 1, #self.tanks do
|
||||
local tank = self.tanks[i] ---@type unit_session
|
||||
local db = tank.get_db() ---@type dynamicv_session_db
|
||||
status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks }
|
||||
end
|
||||
|
||||
-- radiation monitors (environment detectors)
|
||||
status.rad_mon = {}
|
||||
for i = 1, #self.envd do
|
||||
local envd = self.envd[i] ---@type unit_session
|
||||
status.rad_mon[envd.get_device_idx()] = {
|
||||
envd.is_faulted(),
|
||||
envd.get_db().radiation
|
||||
}
|
||||
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
|
||||
end
|
||||
|
||||
return status
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--
|
||||
-- Main SCADA Coordinator GUI
|
||||
-- Supervisor Front Panel GUI
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
@@ -29,7 +29,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
-- create new main view
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
local function init(panel)
|
||||
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
@@ -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 databus = require("supervisor.databus")
|
||||
@@ -16,8 +15,6 @@ local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||
|
||||
-- retry time constants in ms
|
||||
@@ -258,6 +255,18 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
facility.ack_all()
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
if pkt.length == 2 then
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set waste mode packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
if pkt.length == 2 then
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set pu fallback packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command unknown")
|
||||
end
|
||||
@@ -294,9 +303,9 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
|
||||
unit.set_waste(pkt.data[3])
|
||||
unit.set_waste_mode(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command set waste missing option")
|
||||
log.debug(log_header .. "CRDN unit command set waste missing/invalid option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_all()
|
||||
@@ -394,7 +403,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
local builds = {}
|
||||
|
||||
local unit = self.units[unit_id] ---@type reactor_unit
|
||||
builds[unit_id] = unit.get_build(true, false, false)
|
||||
builds[unit_id] = unit.get_build(-1)
|
||||
|
||||
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
|
||||
elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then
|
||||
@@ -408,7 +417,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
local builds = {}
|
||||
|
||||
local unit = self.units[unit_id] ---@type reactor_unit
|
||||
builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPE.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPE.TURBINE_VALVE)
|
||||
builds[unit_id] = unit.get_build(cmd.val.type)
|
||||
|
||||
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
|
||||
else
|
||||
@@ -417,7 +426,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
self.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD
|
||||
self.acks.fac_builds = false
|
||||
|
||||
_send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) })
|
||||
_send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) })
|
||||
end
|
||||
else
|
||||
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)
|
||||
|
||||
@@ -313,26 +313,31 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
|
||||
if pkt.type == RPLC_TYPE.STATUS then
|
||||
-- status packet received, update data
|
||||
if pkt.length >= 5 then
|
||||
self.sDB.last_status_update = pkt.data[1]
|
||||
self.sDB.control_state = pkt.data[2]
|
||||
self.sDB.no_reactor = pkt.data[3]
|
||||
self.sDB.formed = pkt.data[4]
|
||||
self.sDB.auto_ack_token = pkt.data[5]
|
||||
if (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and (type(pkt.data[3]) == "boolean") and
|
||||
(type(pkt.data[4]) == "boolean") and (type(pkt.data[5]) == "number") then
|
||||
self.sDB.last_status_update = pkt.data[1]
|
||||
self.sDB.control_state = pkt.data[2]
|
||||
self.sDB.no_reactor = pkt.data[3]
|
||||
self.sDB.formed = pkt.data[4]
|
||||
self.sDB.auto_ack_token = pkt.data[5]
|
||||
|
||||
if not self.sDB.no_reactor and self.sDB.formed then
|
||||
self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
|
||||
if (not self.sDB.no_reactor) and self.sDB.formed and (type(pkt.data[6]) == "number") then
|
||||
self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
|
||||
|
||||
-- attempt to read mek_data table
|
||||
if pkt.data[7] ~= nil then
|
||||
local status = pcall(_copy_status, pkt.data[7])
|
||||
if status then
|
||||
-- copied in status data OK
|
||||
self.received_status_cache = true
|
||||
else
|
||||
-- error copying status data
|
||||
log.error(log_header .. "failed to parse status packet data")
|
||||
-- attempt to read mek_data table
|
||||
if type(pkt.data[7]) == "table" then
|
||||
local status = pcall(_copy_status, pkt.data[7])
|
||||
if status then
|
||||
-- copied in status data OK
|
||||
self.received_status_cache = true
|
||||
else
|
||||
-- error copying status data
|
||||
log.error(log_header .. "failed to parse status packet data")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet invalid")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet length mismatch")
|
||||
|
||||
@@ -11,6 +11,7 @@ local svqtypes = require("supervisor.session.svqtypes")
|
||||
-- supervisor rtu sessions (svrs)
|
||||
local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
local svrs_boilerv = require("supervisor.session.rtu.boilerv")
|
||||
local svrs_dynamicv = require("supervisor.session.rtu.dynamicv")
|
||||
local svrs_envd = require("supervisor.session.rtu.envd")
|
||||
local svrs_imatrix = require("supervisor.session.rtu.imatrix")
|
||||
local svrs_redstone = require("supervisor.session.rtu.redstone")
|
||||
@@ -138,6 +139,14 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
||||
-- turbine
|
||||
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
-- dynamic tank
|
||||
unit = svrs_dynamicv.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_tank(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.SNA then
|
||||
-- solar neutron activator
|
||||
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_sna(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
-- environment detector
|
||||
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
||||
@@ -161,9 +170,11 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
||||
elseif u_type == RTU_UNIT_TYPE.SPS then
|
||||
-- super-critical phase shifter
|
||||
unit = svrs_sps.new(id, i, unit_advert, self.modbus_q)
|
||||
elseif u_type == RTU_UNIT_TYPE.SNA then
|
||||
-- solar neutron activator
|
||||
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then facility.add_sps(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
-- dynamic tank
|
||||
unit = svrs_dynamicv.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then facility.add_tank(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
-- environment detector
|
||||
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
||||
|
||||
289
supervisor/session/rtu/dynamicv.lua
Normal file
289
supervisor/session/rtu/dynamicv.lua
Normal file
@@ -0,0 +1,289 @@
|
||||
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 qtypes = require("supervisor.session.rtu.qtypes")
|
||||
local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local dynamicv = {}
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local DTV_RTU_S_CMDS = qtypes.DTV_RTU_S_CMDS
|
||||
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
|
||||
|
||||
local TXN_TYPES = {
|
||||
FORMED = 1,
|
||||
BUILD = 2,
|
||||
STATE = 3,
|
||||
TANKS = 4,
|
||||
INC_CONT = 5,
|
||||
DEC_CONT = 6,
|
||||
SET_CONT = 7
|
||||
}
|
||||
|
||||
local TXN_TAGS = {
|
||||
"dynamicv.formed",
|
||||
"dynamicv.build",
|
||||
"dynamicv.state",
|
||||
"dynamicv.tanks",
|
||||
"dynamicv.inc_cont_mode",
|
||||
"dynamicv.dec_cont_mode",
|
||||
"dynamicv.set_cont_mode"
|
||||
}
|
||||
|
||||
local PERIODICS = {
|
||||
FORMED = 2000,
|
||||
BUILD = 1000,
|
||||
STATE = 1000,
|
||||
TANKS = 500
|
||||
}
|
||||
|
||||
-- create a new dynamicv rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
function dynamicv.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
log.error("attempt to instantiate dynamicv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
local log_tag = "session.rtu(" .. session_id .. ").dynamicv(" .. advert.index .. "): "
|
||||
|
||||
local self = {
|
||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||
has_build = false,
|
||||
periodics = {
|
||||
next_formed_req = 0,
|
||||
next_build_req = 0,
|
||||
next_state_req = 0,
|
||||
next_tanks_req = 0
|
||||
},
|
||||
---@class dynamicv_session_db
|
||||
db = {
|
||||
formed = false,
|
||||
build = {
|
||||
last_update = 0,
|
||||
length = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
min_pos = types.new_zero_coordinate(),
|
||||
max_pos = types.new_zero_coordinate(),
|
||||
tank_capacity = 0,
|
||||
chem_tank_capacity = 0
|
||||
},
|
||||
state = {
|
||||
last_update = 0,
|
||||
container_mode = CONTAINER_MODE.BOTH ---@type container_mode
|
||||
},
|
||||
tanks = {
|
||||
last_update = 0,
|
||||
stored = types.new_empty_gas(),
|
||||
fill = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local public = self.session.get()
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- increment the container mode
|
||||
local function _inc_cont_mode()
|
||||
-- write coil 1 with unused value 0
|
||||
self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
|
||||
end
|
||||
|
||||
-- decrement the container mode
|
||||
local function _dec_cont_mode()
|
||||
-- write coil 2 with unused value 0
|
||||
self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
|
||||
end
|
||||
|
||||
-- set the container mode
|
||||
---@param mode container_mode
|
||||
local function _set_cont_mode(mode)
|
||||
-- write holding register 1
|
||||
self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
|
||||
end
|
||||
|
||||
-- query if the multiblock is formed
|
||||
local function _request_formed()
|
||||
-- read discrete input 1 (start = 1, count = 1)
|
||||
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
|
||||
end
|
||||
|
||||
-- query the build of the device
|
||||
local function _request_build()
|
||||
-- read input registers 1 through 7 (start = 1, count = 7)
|
||||
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
|
||||
end
|
||||
|
||||
-- query the state of the device
|
||||
local function _request_state()
|
||||
-- read holding register 1 (start = 1, count = 1)
|
||||
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 })
|
||||
end
|
||||
|
||||
-- query the tanks of the device
|
||||
local function _request_tanks()
|
||||
-- read input registers 8 through 9 (start = 8, count = 2)
|
||||
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- handle a packet
|
||||
---@param m_pkt modbus_frame
|
||||
function public.handle_packet(m_pkt)
|
||||
local txn_type = self.session.try_resolve(m_pkt)
|
||||
if txn_type == false then
|
||||
-- nothing to do
|
||||
elseif txn_type == TXN_TYPES.FORMED then
|
||||
-- formed response
|
||||
-- load in data if correct length
|
||||
if m_pkt.length == 1 then
|
||||
self.db.formed = m_pkt.data[1]
|
||||
|
||||
if not self.db.formed then self.has_build = false end
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
end
|
||||
elseif txn_type == TXN_TYPES.BUILD then
|
||||
-- build response
|
||||
if m_pkt.length == 7 then
|
||||
self.db.build.last_update = util.time_ms()
|
||||
self.db.build.length = m_pkt.data[1]
|
||||
self.db.build.width = m_pkt.data[2]
|
||||
self.db.build.height = m_pkt.data[3]
|
||||
self.db.build.min_pos = m_pkt.data[4]
|
||||
self.db.build.max_pos = m_pkt.data[5]
|
||||
self.db.build.tank_capacity = m_pkt.data[6]
|
||||
self.db.build.chem_tank_capacity = m_pkt.data[7]
|
||||
self.has_build = true
|
||||
|
||||
out_queue.push_data(unit_session.RTU_US_DATA.BUILD_CHANGED, { unit = advert.reactor, type = advert.type })
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
end
|
||||
elseif txn_type == TXN_TYPES.STATE then
|
||||
-- state response
|
||||
if m_pkt.length == 1 then
|
||||
self.db.state.last_update = util.time_ms()
|
||||
self.db.state.container_mode = m_pkt.data[1]
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
end
|
||||
elseif txn_type == TXN_TYPES.TANKS then
|
||||
-- tanks response
|
||||
if m_pkt.length == 2 then
|
||||
self.db.tanks.last_update = util.time_ms()
|
||||
self.db.tanks.stored = m_pkt.data[1]
|
||||
self.db.tanks.fill = m_pkt.data[2]
|
||||
else
|
||||
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
|
||||
end
|
||||
elseif txn_type == TXN_TYPES.INC_CONT or txn_type == TXN_TYPES.DEC_CONT or txn_type == TXN_TYPES.SET_CONT then
|
||||
-- successful acknowledgement
|
||||
elseif txn_type == nil then
|
||||
log.error(log_tag .. "unknown transaction reply")
|
||||
else
|
||||
log.error(log_tag .. "unknown transaction type " .. txn_type)
|
||||
end
|
||||
end
|
||||
|
||||
-- update this runner
|
||||
---@param time_now integer milliseconds
|
||||
function public.update(time_now)
|
||||
-- check command queue
|
||||
while self.session.in_q.ready() do
|
||||
-- get a new message to process
|
||||
local msg = self.session.in_q.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- instruction
|
||||
local cmd = msg.message
|
||||
|
||||
if cmd == DTV_RTU_S_CMDS.INC_CONT_MODE then
|
||||
_inc_cont_mode()
|
||||
elseif cmd == DTV_RTU_S_CMDS.DEC_CONT_MODE then
|
||||
_dec_cont_mode()
|
||||
else
|
||||
log.debug(util.c(log_tag, "unrecognized in-queue command ", cmd))
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- instruction with body
|
||||
local cmd = msg.message ---@type queue_data
|
||||
if cmd.key == DTV_RTU_S_DATA.SET_CONT_MODE then
|
||||
if cmd.val == types.CONTAINER_MODE.BOTH or
|
||||
cmd.val == types.CONTAINER_MODE.FILL or
|
||||
cmd.val == types.CONTAINER_MODE.EMPTY then
|
||||
_set_cont_mode(cmd.val)
|
||||
else
|
||||
log.debug(util.c(log_tag, "unrecognized container mode \"", cmd.val, "\""))
|
||||
end
|
||||
else
|
||||
log.debug(util.c(log_tag, "unrecognized in-queue data ", cmd.key))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - time_now > 100 then
|
||||
log.warning(log_tag .. "exceeded 100ms queue process limit")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
time_now = util.time()
|
||||
|
||||
-- handle periodics
|
||||
|
||||
if self.periodics.next_formed_req <= time_now then
|
||||
_request_formed()
|
||||
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
|
||||
end
|
||||
|
||||
if self.db.formed then
|
||||
if not self.has_build and self.periodics.next_build_req <= time_now then
|
||||
_request_build()
|
||||
self.periodics.next_build_req = time_now + PERIODICS.BUILD
|
||||
end
|
||||
|
||||
if self.periodics.next_state_req <= time_now then
|
||||
_request_state()
|
||||
self.periodics.next_state_req = time_now + PERIODICS.STATE
|
||||
end
|
||||
|
||||
if self.periodics.next_tanks_req <= time_now then
|
||||
_request_tanks()
|
||||
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
|
||||
end
|
||||
end
|
||||
|
||||
self.session.post_update()
|
||||
end
|
||||
|
||||
-- invalidate build cache
|
||||
function public.invalidate_cache()
|
||||
self.periodics.next_formed_req = 0
|
||||
self.periodics.next_build_req = 0
|
||||
self.has_build = false
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return dynamicv
|
||||
@@ -1,16 +1,31 @@
|
||||
---@class rtu_unit_qtypes
|
||||
local qtypes = {}
|
||||
|
||||
-- turbine valve rtu session commands
|
||||
local TBV_RTU_S_CMDS = {
|
||||
INC_DUMP_MODE = 1,
|
||||
DEC_DUMP_MODE = 2
|
||||
}
|
||||
|
||||
-- turbine valve rtu session commands w/ parameters
|
||||
local TBV_RTU_S_DATA = {
|
||||
SET_DUMP_MODE = 1
|
||||
}
|
||||
|
||||
-- dynamic tank valve rtu session commands
|
||||
local DTV_RTU_S_CMDS = {
|
||||
INC_CONT_MODE = 1,
|
||||
DEC_CONT_MODE = 2
|
||||
}
|
||||
|
||||
-- dynamic tank valve rtu session commands w/ parameters
|
||||
local DTV_RTU_S_DATA = {
|
||||
SET_CONT_MODE = 1
|
||||
}
|
||||
|
||||
qtypes.TBV_RTU_S_CMDS = TBV_RTU_S_CMDS
|
||||
qtypes.TBV_RTU_S_DATA = TBV_RTU_S_DATA
|
||||
qtypes.DTV_RTU_S_CMDS = DTV_RTU_S_CMDS
|
||||
qtypes.DTV_RTU_S_DATA = DTV_RTU_S_DATA
|
||||
|
||||
return qtypes
|
||||
|
||||
@@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
|
||||
|
||||
local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local SUPERVISOR_VERSION = "v0.18.0"
|
||||
local SUPERVISOR_VERSION = "v0.20.2"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -116,7 +116,7 @@ local function main()
|
||||
if not fp_ok then
|
||||
renderer.close_ui()
|
||||
println_ts(util.c("UI error: ", message))
|
||||
log.error(util.c("GUI crashed with error ", message))
|
||||
log.error(util.c("front panel GUI render failed with error ", message))
|
||||
else
|
||||
-- redefine println_ts local to not print as we have the front panel running
|
||||
println_ts = function (_) end
|
||||
@@ -153,7 +153,13 @@ local function main()
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.warning("comms modem disconnected")
|
||||
|
||||
databus.tx_hw_modem(false)
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log.info("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
databus.tx_hw_modem(false)
|
||||
end
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
@@ -164,7 +170,7 @@ local function main()
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() and not nic.connected() then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
nic.connect(device)
|
||||
|
||||
@@ -172,6 +178,8 @@ local function main()
|
||||
log.info("comms modem reconnected")
|
||||
|
||||
databus.tx_hw_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log.info("wired modem reconnected")
|
||||
end
|
||||
|
||||
@@ -11,11 +11,13 @@ local rsctl = require("supervisor.session.rsctl")
|
||||
---@class reactor_control_unit
|
||||
local unit = {}
|
||||
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
|
||||
@@ -68,10 +70,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
num_turbines = num_turbines,
|
||||
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
|
||||
-- rtus
|
||||
rtu_list = {},
|
||||
redstone = {},
|
||||
boilers = {},
|
||||
turbines = {},
|
||||
tanks = {},
|
||||
snas = {},
|
||||
envd = {},
|
||||
sna_prod_rate = 0,
|
||||
-- redstone control
|
||||
io_ctl = nil, ---@type rs_controller
|
||||
valves = {}, ---@type unit_valves
|
||||
@@ -89,7 +95,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
damage_start = 0,
|
||||
damage_last = 0,
|
||||
damage_est_last = 0,
|
||||
waste_mode = WASTE_MODE.AUTO,
|
||||
waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
|
||||
status_text = { "UNKNOWN", "awaiting connection..." },
|
||||
-- logic for alarms
|
||||
had_reactor = false,
|
||||
@@ -221,11 +227,15 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
degraded = false,
|
||||
blade_count = 0,
|
||||
br100 = 0,
|
||||
lim_br100 = 0
|
||||
lim_br100 = 0,
|
||||
waste_mode = WASTE_MODE.AUTO ---@type WASTE_MODE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-- list for RTU session management
|
||||
self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd }
|
||||
|
||||
-- init redstone RTU I/O controller
|
||||
self.io_ctl = rsctl.new(self.redstone)
|
||||
|
||||
@@ -341,14 +351,34 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
emer_cool = emer_cool
|
||||
}
|
||||
|
||||
--#endregion
|
||||
-- route reactor waste for a given waste product
|
||||
---@param product WASTE_PRODUCT waste product to route valves for
|
||||
local function _set_waste_valves(product)
|
||||
self.waste_product = product
|
||||
|
||||
-- unlink disconnected units
|
||||
---@param sessions table
|
||||
local function _unlink_disconnected_units(sessions)
|
||||
util.filter_table(sessions, function (u) return u.is_connected() end)
|
||||
if product == WASTE.PLUTONIUM then
|
||||
-- route through plutonium generation
|
||||
waste_pu.open()
|
||||
waste_sna.close()
|
||||
waste_po.close()
|
||||
waste_sps.close()
|
||||
elseif product == WASTE.POLONIUM then
|
||||
-- route through polonium generation into pellets
|
||||
waste_pu.close()
|
||||
waste_sna.open()
|
||||
waste_po.open()
|
||||
waste_sps.close()
|
||||
elseif product == WASTE.ANTI_MATTER then
|
||||
-- route through polonium generation into SPS
|
||||
waste_pu.close()
|
||||
waste_sna.open()
|
||||
waste_po.close()
|
||||
waste_sps.open()
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class reactor_unit
|
||||
@@ -378,11 +408,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
table.insert(self.redstone, rs_unit)
|
||||
|
||||
-- send or re-send waste settings
|
||||
public.set_waste(self.waste_mode)
|
||||
_set_waste_valves(self.waste_product)
|
||||
end
|
||||
|
||||
-- link a turbine RTU session
|
||||
---@param turbine unit_session
|
||||
---@return boolean linked turbine accepted to associated device slot
|
||||
function public.add_turbine(turbine)
|
||||
if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then
|
||||
table.insert(self.turbines, turbine)
|
||||
@@ -392,13 +423,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
_reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx())
|
||||
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- link a boiler RTU session
|
||||
---@param boiler unit_session
|
||||
---@return boolean linked boiler accepted to associated device slot
|
||||
function public.add_boiler(boiler)
|
||||
if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then
|
||||
table.insert(self.boilers, boiler)
|
||||
@@ -410,24 +440,37 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
_reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx())
|
||||
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- link a dynamic tank RTU session
|
||||
---@param dynamic_tank unit_session
|
||||
---@return boolean linked dynamic tank accepted (max 1)
|
||||
function public.add_tank(dynamic_tank)
|
||||
if #self.tanks == 0 then
|
||||
table.insert(self.tanks, dynamic_tank)
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- link a solar neutron activator RTU session
|
||||
---@param sna unit_session
|
||||
function public.add_sna(sna) table.insert(self.snas, sna) end
|
||||
|
||||
-- link an environment detector RTU session
|
||||
---@param envd unit_session
|
||||
---@return boolean linked environment detector accepted (max 1)
|
||||
function public.add_envd(envd)
|
||||
table.insert(self.envd, envd)
|
||||
if #self.envd == 0 then
|
||||
table.insert(self.envd, envd)
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-- purge devices associated with the given RTU session ID
|
||||
---@param session integer RTU session ID
|
||||
function public.purge_rtu_devices(session)
|
||||
util.filter_table(self.redstone, function (s) return s.get_session_id() ~= session end)
|
||||
util.filter_table(self.boilers, function (s) return s.get_session_id() ~= session end)
|
||||
util.filter_table(self.turbines, function (s) return s.get_session_id() ~= session end)
|
||||
util.filter_table(self.envd, function (s) return s.get_session_id() ~= session end)
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (s) return s.get_session_id() ~= session end) end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
@@ -445,10 +488,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
end
|
||||
|
||||
-- unlink RTU unit sessions if they are closed
|
||||
_unlink_disconnected_units(self.redstone)
|
||||
_unlink_disconnected_units(self.boilers)
|
||||
_unlink_disconnected_units(self.turbines)
|
||||
_unlink_disconnected_units(self.envd)
|
||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
||||
|
||||
-- update degraded state for auto control
|
||||
self.db.control.degraded = (#self.boilers ~= num_boilers) or (#self.turbines ~= num_turbines) or (self.plc_i == nil)
|
||||
@@ -577,6 +617,15 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
end
|
||||
end
|
||||
|
||||
-- set automatic waste product if mode is set to auto
|
||||
---@param product WASTE_PRODUCT waste product to generate
|
||||
function public.auto_set_waste(product)
|
||||
if self.db.control.waste_mode == WASTE_MODE.AUTO then
|
||||
self.waste_product = product
|
||||
_set_waste_valves(product)
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- OPERATIONS --
|
||||
@@ -621,34 +670,18 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
end
|
||||
end
|
||||
|
||||
-- route reactor waste
|
||||
---@param mode WASTE_MODE waste handling mode
|
||||
function public.set_waste(mode)
|
||||
if mode == WASTE_MODE.AUTO then
|
||||
---@todo automatic waste routing
|
||||
self.waste_mode = mode
|
||||
elseif mode == WASTE_MODE.PLUTONIUM then
|
||||
-- route through plutonium generation
|
||||
self.waste_mode = mode
|
||||
waste_pu.open()
|
||||
waste_sna.close()
|
||||
waste_po.close()
|
||||
waste_sps.close()
|
||||
elseif mode == WASTE_MODE.POLONIUM then
|
||||
-- route through polonium generation into pellets
|
||||
self.waste_mode = mode
|
||||
waste_pu.close()
|
||||
waste_sna.open()
|
||||
waste_po.open()
|
||||
waste_sps.close()
|
||||
elseif mode == WASTE_MODE.ANTI_MATTER then
|
||||
-- route through polonium generation into SPS
|
||||
self.waste_mode = mode
|
||||
waste_pu.close()
|
||||
waste_sna.open()
|
||||
waste_po.close()
|
||||
waste_sps.open()
|
||||
else
|
||||
-- set waste processing mode
|
||||
---@param mode WASTE_MODE processing mode
|
||||
function public.set_waste_mode(mode)
|
||||
self.db.control.waste_mode = mode
|
||||
|
||||
if mode == WASTE_MODE.MANUAL_PLUTONIUM then
|
||||
_set_waste_valves(WASTE.PLUTONIUM)
|
||||
elseif mode == WASTE_MODE.MANUAL_POLONIUM then
|
||||
_set_waste_valves(WASTE.POLONIUM)
|
||||
elseif mode == WASTE_MODE.MANUAL_ANTI_MATTER then
|
||||
_set_waste_valves(WASTE.ANTI_MATTER)
|
||||
elseif mode > WASTE_MODE.MANUAL_ANTI_MATTER then
|
||||
log.debug(util.c("invalid waste mode setting ", mode))
|
||||
end
|
||||
end
|
||||
@@ -686,21 +719,25 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
return false
|
||||
end
|
||||
|
||||
-- get build properties of all machines
|
||||
-- get build properties of machines
|
||||
--
|
||||
-- filter options
|
||||
-- - nil to include all builds
|
||||
-- - -1 to include only PLC build
|
||||
-- - RTU_UNIT_TYPE to include all builds of machines of that type
|
||||
---@nodiscard
|
||||
---@param inc_plc boolean? true/nil to include PLC build, false to exclude
|
||||
---@param inc_boilers boolean? true/nil to include boiler builds, false to exclude
|
||||
---@param inc_turbines boolean? true/nil to include turbine builds, false to exclude
|
||||
function public.get_build(inc_plc, inc_boilers, inc_turbines)
|
||||
---@param filter -1|RTU_UNIT_TYPE? filter as described above
|
||||
function public.get_build(filter)
|
||||
local all = filter == nil
|
||||
local build = {}
|
||||
|
||||
if inc_plc ~= false then
|
||||
if all or (filter == -1) then
|
||||
if self.plc_i ~= nil then
|
||||
build.reactor = self.plc_i.get_struct()
|
||||
end
|
||||
end
|
||||
|
||||
if inc_boilers ~= false then
|
||||
if all or (filter == RTU_UNIT_TYPE.BOILER_VALVE) then
|
||||
build.boilers = {}
|
||||
for i = 1, #self.boilers do
|
||||
local boiler = self.boilers[i] ---@type unit_session
|
||||
@@ -708,7 +745,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
end
|
||||
end
|
||||
|
||||
if inc_turbines ~= false then
|
||||
if all or (filter == RTU_UNIT_TYPE.TURBINE_VALVE) then
|
||||
build.turbines = {}
|
||||
for i = 1, #self.turbines do
|
||||
local turbine = self.turbines[i] ---@type unit_session
|
||||
@@ -716,6 +753,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
end
|
||||
end
|
||||
|
||||
if all or (filter == RTU_UNIT_TYPE.DYNAMIC_VALVE) then
|
||||
build.tanks = {}
|
||||
for i = 1, #self.tanks do
|
||||
local tank = self.tanks[i] ---@type unit_session
|
||||
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
|
||||
end
|
||||
end
|
||||
|
||||
return build
|
||||
end
|
||||
|
||||
@@ -730,6 +775,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
return status
|
||||
end
|
||||
|
||||
-- get the current burn rate (actual rate)
|
||||
---@nodiscard
|
||||
function public.get_burn_rate()
|
||||
local rate = 0
|
||||
if self.plc_i ~= nil then rate = self.plc_i.get_status().act_burn_rate end
|
||||
return rate or 0
|
||||
end
|
||||
|
||||
-- get RTU statuses
|
||||
---@nodiscard
|
||||
function public.get_rtu_statuses()
|
||||
@@ -738,40 +791,54 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
-- status of boilers (including tanks)
|
||||
status.boilers = {}
|
||||
for i = 1, #self.boilers do
|
||||
local boiler = self.boilers[i] ---@type unit_session
|
||||
status.boilers[boiler.get_device_idx()] = {
|
||||
boiler.is_faulted(),
|
||||
boiler.get_db().formed,
|
||||
boiler.get_db().state,
|
||||
boiler.get_db().tanks
|
||||
}
|
||||
local boiler = self.boilers[i] ---@type unit_session
|
||||
local db = boiler.get_db() ---@type boilerv_session_db
|
||||
status.boilers[boiler.get_device_idx()] = { boiler.is_faulted(), db.formed, db.state, db.tanks }
|
||||
end
|
||||
|
||||
-- status of turbines (including tanks)
|
||||
status.turbines = {}
|
||||
for i = 1, #self.turbines do
|
||||
local turbine = self.turbines[i] ---@type unit_session
|
||||
status.turbines[turbine.get_device_idx()] = {
|
||||
turbine.is_faulted(),
|
||||
turbine.get_db().formed,
|
||||
turbine.get_db().state,
|
||||
turbine.get_db().tanks
|
||||
}
|
||||
local turbine = self.turbines[i] ---@type unit_session
|
||||
local db = turbine.get_db() ---@type turbinev_session_db
|
||||
status.turbines[turbine.get_device_idx()] = { turbine.is_faulted(), db.formed, db.state, db.tanks }
|
||||
end
|
||||
|
||||
-- status of dynamic tanks
|
||||
status.tanks = {}
|
||||
for i = 1, #self.tanks do
|
||||
local tank = self.tanks[i] ---@type unit_session
|
||||
local db = tank.get_db() ---@type dynamicv_session_db
|
||||
status.tanks[tank.get_device_idx()] = { tank.is_faulted(), db.formed, db.state, db.tanks }
|
||||
end
|
||||
|
||||
-- basic SNA statistical information
|
||||
status.sna = { #self.snas, public.get_sna_rate() }
|
||||
|
||||
-- radiation monitors (environment detectors)
|
||||
status.rad_mon = {}
|
||||
for i = 1, #self.envd do
|
||||
local envd = self.envd[i] ---@type unit_session
|
||||
status.rad_mon[envd.get_device_idx()] = {
|
||||
envd.is_faulted(),
|
||||
envd.get_db().radiation
|
||||
}
|
||||
local envd = self.envd[i] ---@type unit_session
|
||||
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
|
||||
end
|
||||
|
||||
return status
|
||||
end
|
||||
|
||||
-- get the current total [max] production rate is
|
||||
---@nodiscard
|
||||
---@return number total_avail_rate
|
||||
function public.get_sna_rate()
|
||||
local total_avail_rate = 0
|
||||
|
||||
for i = 1, #self.snas do
|
||||
local db = self.snas[i].get_db() ---@type sna_session_db
|
||||
total_avail_rate = total_avail_rate + db.state.production_rate
|
||||
end
|
||||
|
||||
return total_avail_rate
|
||||
end
|
||||
|
||||
-- get the annunciator status
|
||||
---@nodiscard
|
||||
function public.get_annunciator() return self.db.annunciator end
|
||||
@@ -787,7 +854,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
-- get unit state
|
||||
---@nodiscard
|
||||
function public.get_state()
|
||||
return { self.status_text[1], self.status_text[2], self.waste_mode, self.db.control.ready, self.db.control.degraded }
|
||||
return {
|
||||
self.status_text[1],
|
||||
self.status_text[2],
|
||||
self.db.control.ready,
|
||||
self.db.control.degraded,
|
||||
self.db.control.waste_mode,
|
||||
self.waste_product
|
||||
}
|
||||
end
|
||||
|
||||
-- get the reactor ID
|
||||
|
||||
Reference in New Issue
Block a user