Compare commits

..

48 Commits

Author SHA1 Message Date
Mikayla
bdbb3071b3 Merge pull request #294 from MikaylaFischler/devel
2023.07.19 Hotfix
2023-07-19 19:18:48 -04:00
Mikayla Fischler
def02a94d2 #293 fixed race condition with graphics element IDs 2023-07-19 11:27:33 -04:00
Mikayla Fischler
681bb0963e #291 RTU comms thread no longer yields every packet 2023-07-18 22:28:43 -04:00
Mikayla
8f7d7c3ead Merge pull request #288 from MikaylaFischler/devel
2023.07.17 Hotfix
2023-07-17 22:48:03 -04:00
Mikayla Fischler
c0f45cfb8b updated comments 2023-07-17 22:47:19 -04:00
Mikayla Fischler
455653074a #287 fixed coordinator not notifying supervisor of auto waste config 2023-07-17 22:09:21 -04:00
Mikayla Fischler
1202289fab #285 #286 mitigated false trips 2023-07-17 20:59:45 -04:00
Mikayla
acb7b5b4cb update README.md with new installer pastebin 2023-07-16 21:29:41 -04:00
Mikayla
9bd79dacad Merge pull request #281 from MikaylaFischler/devel
2023.07.16 Release
2023-07-16 21:25:00 -04:00
Mikayla Fischler
c544d140bf installer key handling improvements 2023-07-16 21:21:33 -04:00
Mikayla Fischler
353cb3622b improved installer any key detection 2023-07-16 21:11:27 -04:00
Mikayla Fischler
b54f15bad6 #274 bugfixes and optimizations 2023-07-16 21:07:37 -04:00
Mikayla Fischler
4d9783beca fixed installer clean bug 2023-07-16 20:58:34 -04:00
Mikayla Fischler
5529774b0e changed installer press enter to continue to any key, fixed some text colors 2023-07-16 20:53:39 -04:00
Mikayla Fischler
2a541ef3fe #274 cleanup functionality added to installer 2023-07-16 19:42:20 -04:00
Mikayla Fischler
e1b4d72ef8 updated legacy install manifest 2023-07-15 13:33:51 -04:00
Mikayla Fischler
6a0992c7a4 removed unused variable 2023-07-15 13:33:18 -04:00
Mikayla Fischler
cff7c724be #272 fixed bug with transmitting unit dynamic tank table 2023-07-15 13:31:48 -04:00
Mikayla Fischler
47bda73afe #272 basic dynamic tank data in supervisor and coordinator 2023-07-15 13:16:36 -04:00
Mikayla
8daedc109c added discord info to readme 2023-07-13 13:50:17 -04:00
Mikayla Fischler
a164c18a50 removed unused fields from dynamic tank rtu 2023-07-13 12:19:25 -04:00
Mikayla Fischler
4d663ada8d added high contrast yellow to rtu/plc/coord front panels 2023-07-12 13:38:37 -04:00
Mikayla Fischler
084a153a79 #268 fixed incorrect info print on extra wireless modem connection 2023-07-11 21:06:47 -04:00
Mikayla Fischler
4ed6ec1c63 correctly print new messages without overwrites in dmesg even if a prior message is a progress one 2023-07-11 21:01:24 -04:00
Mikayla Fischler
d3c2ba7bee update install manifest 2023-07-11 20:32:37 -04:00
Mikayla Fischler
55ff9dad4b #249 coordinator handle monitor disconnects/reconnects 2023-07-11 20:32:10 -04:00
Mikayla Fischler
0d6022f5e3 fixed always reporting failure to connect to supervisor even when inaccurate 2023-07-11 18:31:53 -04:00
Mikayla Fischler
8b136d78a8 #268 better handling of wireless modem peripherals 2023-07-11 18:22:09 -04:00
Mikayla Fischler
a5214730ef #260 added dynamic tank RTU 2023-07-11 17:27:03 -04:00
Mikayla Fischler
9f3ad3caf0 removed PLC establish packet handling when already linked 2023-07-11 16:17:24 -04:00
Mikayla
9bb2a99be5 Merge pull request #279 from MikaylaFischler/265-coordinator-front-panel
265 coordinator front panel
2023-07-11 15:37:17 -04:00
Mikayla Fischler
65ace26258 corrected comments 2023-07-11 15:36:41 -04:00
Mikayla Fischler
61d975d13f updated error messages for consistency 2023-07-11 15:15:44 -04:00
Mikayla Fischler
1d7d6e9817 update legacy install manifest 2023-07-11 13:38:21 -04:00
Mikayla Fischler
a2e0999cea combine coordinator supervisor connection event loop with main loop 2023-07-11 13:32:26 -04:00
Mikayla Fischler
1edee7f64b updated graphics comments 2023-07-09 23:42:44 -04:00
Mikayla Fischler
df61ec2c62 #265 coordinator front panel 2023-07-09 23:31:56 -04:00
Mikayla Fischler
bf7a316b04 don't start flasher if already started 2023-07-09 23:24:41 -04:00
Mikayla Fischler
96c4444184 corrected some comments 2023-07-09 23:22:24 -04:00
Mikayla Fischler
59eac62c33 #270 validate reactor PLC status packet types 2023-07-08 18:07:40 -04:00
Mikayla
ab193db153 Merge pull request #277 from MikaylaFischler/25-process-waste-control
25 process waste control
2023-07-08 17:12:23 -04:00
Mikayla Fischler
7d65bba589 fixes/cleanups for pull request 2023-07-08 17:11:51 -04:00
Mikayla Fischler
dcef5a96f0 removed unused function 2023-07-08 16:57:41 -04:00
Mikayla Fischler
ba0900ac65 #25 sna/sps integration, plutonium fallback, waste rate reporting 2023-07-08 16:57:13 -04:00
Mikayla Fischler
8f54e95519 #25 continued WIP waste control, main view updated and unit fields modified 2023-07-06 01:36:06 -04:00
Mikayla Fischler
7b9824b6f9 added checkbox graphics element 2023-07-01 19:40:33 -04:00
Mikayla Fischler
b6835fc7d1 #276 updated readme 2023-06-29 16:54:46 -04:00
Mikayla
bc5a94cd3b Update README.md 2023-06-29 12:57:25 -04:00
80 changed files with 2565 additions and 864 deletions

View File

@@ -7,6 +7,27 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=latest&label=latest)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel)
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
![Discord](https://img.shields.io/discord/1129075839288496259)
## Released Component Versions
![Installer](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Finstaller.json)
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
![Reactor PLC](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Freactor-plc.json)
![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
![Supervisor](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fsupervisor.json)
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.json)
![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## 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
![Bootloader](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fbootloader.json)
![Comms](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcomms.json)
### Utilities
![Installer](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Finstaller.json)
### Applications
![Reactor PLC](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Freactor-plc.json)
![RTU](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Frtu.json)
![Supervisor](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fsupervisor.json)
![Coordinator](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fcoordinator.json)
![Pocket](https://img.shields.io/endpoint?url=https%3A%2F%2Fmikaylafischler.github.io%2Fcc-mek-scada%2Fpocket.json)
## 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
View File

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

View File

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

View File

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

View File

@@ -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
@@ -68,20 +75,24 @@ function process.init(iocontrol, coord_comms)
end
log.info("PROCESS: loaded auto control settings from coord.settings")
-- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback)
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 +148,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 +164,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 +215,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 +245,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 +276,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 +299,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

View File

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

View File

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

View File

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

View File

@@ -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.2"
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

View 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

View File

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

View File

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

View 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

View File

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

60
coordinator/ui/pgi.lua Normal file
View File

@@ -0,0 +1,60 @@
--
-- 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
else
log.debug(util.c("PGI: tried to delete unknown PKT entry ", session_id))
end
end
return pgi

View File

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

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "1.0.0"
core.version = "1.0.2"
core.flasher = flasher
core.events = events

View File

@@ -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
@@ -70,7 +71,8 @@ function element.new(args, child_offset_x, child_offset_y)
p_window = nil, ---@type table
position = { x = 1, y = 1 }, ---@type coordinate_2d
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
next_y = 1,
next_y = 1, -- next child y coordinate
next_id = 0, -- next child ID
subscriptions = {},
mt = {}
}
@@ -351,13 +353,14 @@ function element.new(args, child_offset_x, child_offset_y)
local child_element = child.get()
if key == nil then
table.insert(protected.children, child_element)
return #protected.children
else
protected.children[key] = child_element
return key
local id = key ---@type string|integer|nil
if id == nil then
id = self.next_id
self.next_id = self.next_id + 1
end
protected.children[id] = child_element
return id
end
-- remove a child element

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.5"
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

View File

@@ -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)
@@ -298,7 +308,9 @@ function threads.thread__comms(smem)
-- thread loop
while true do
-- check for messages in the message queue
local handle_start = util.time()
-- check for messages in the message queue while not shut down
while comms_queue.ready() and not rtu_state.shutdown do
local msg = comms_queue.pop()
@@ -314,10 +326,16 @@ function threads.thread__comms(smem)
end
end
-- quick yield
util.nop()
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning("comms thread exceeded 100ms queue process limit")
break
end
end
-- quick yield
util.nop()
-- check for termination request
if rtu_state.shutdown then
rtu_comms.close(rtu_state)
@@ -404,6 +422,12 @@ function threads.thread__unit_comms(smem, unit)
util.nop()
end
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. short_name)
break
end
-- check if multiblock is still formed if this is a multiblock
if unit.is_multiblock and (util.time_ms() - last_f_check > 250) then
local is_formed = unit.device.isFormed()
@@ -441,6 +465,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
@@ -491,12 +521,6 @@ function threads.thread__unit_comms(smem, unit)
-- update hw status
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. short_name)
break
end
-- delay before next check
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,6 +60,8 @@ function pgi.delete_rtu_entry(session_id)
if not success then
log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
end
else
log.debug(util.c("PGI: tried to delete unknown RTU entry ", session_id))
end
end
@@ -87,6 +89,8 @@ function pgi.delete_pdg_entry(session_id)
if not success then
log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
end
else
log.debug(util.c("PGI: tried to delete unknown PDG entry ", session_id))
end
end

View File

@@ -1,7 +1,6 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local 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)

View File

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

View File

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

View 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

View File

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

View File

@@ -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.4"
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

View File

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

View File

@@ -327,13 +327,14 @@ function logic.update_annunciator(self)
--[[
Generator Trip
a generator trip is when a generator suddenly and unexpectedly loses it's external load
oftentimes this is when a power plant is disconnected from the grid for one reason or another
in this case we just:
- check if internal power storage of turbine is increasing
that means there is no external load and there will be a turbine trip soon if this is not resolved
a generator trip is when a generator suddenly and unexpectedly loses it's external load, which occurs when a power plant
is disconnected from the grid. in our case, this is when the turbine is disconnected, or what it's connected to becomes
fully charged. this is identified by detecting if:
- the internal power storage of the turbine is increasing AND
- there is at least 5% energy fill (preventing false trips with periodic power extraction from other mods)
this would then mean there is no external load and there will be a turbine trip soon if this is not resolved
]]--
self.db.annunciator.GeneratorTrip[idx] = _get_dt(DT_KEYS.TurbinePower .. idx) > 0.0
self.db.annunciator.GeneratorTrip[idx] = (_get_dt(DT_KEYS.TurbinePower .. idx) > 0.0) and (db.tanks.energy_fill > 0.05)
--[[
Turbine Trip
@@ -504,9 +505,6 @@ function logic.update_alarms(self)
local rcs_trans = any_low or any_over or gen_trip or annunc.RCPTrip or annunc.MaxWaterReturnFeed
-- only care about RCS flow low early with boilers
if self.num_boilers > 0 then rcs_trans = rcs_trans or annunc.RCSFlowLow end
-- annunciator indicators for these states may not indicate a real issue when:
-- > flow is ramping up right after reactor start
-- > flow is ramping down after reactor shutdown