Compare commits
146 Commits
v1.8.1-bet
...
v1.8.8-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8347afb6d0 | ||
|
|
10d0a9763a | ||
|
|
96691d773a | ||
|
|
910509d764 | ||
|
|
158cc39b80 | ||
|
|
940f71aa35 | ||
|
|
788cef8f86 | ||
|
|
baaef862ab | ||
|
|
96db709ced | ||
|
|
c47fa5433f | ||
|
|
440b724798 | ||
|
|
126d6eb163 | ||
|
|
8ac46faf36 | ||
|
|
f112746e12 | ||
|
|
76f21e925b | ||
|
|
a330249c7e | ||
|
|
bbc64c8dc2 | ||
|
|
bb062cf397 | ||
|
|
53bb36ce8d | ||
|
|
8237113577 | ||
|
|
02906ae707 | ||
|
|
6d0e777e68 | ||
|
|
36468c4043 | ||
|
|
1cf7375311 | ||
|
|
ca55948286 | ||
|
|
195f59178f | ||
|
|
e416faf313 | ||
|
|
20bff48cfd | ||
|
|
1a9892b291 | ||
|
|
827953c0a1 | ||
|
|
56e69e3a29 | ||
|
|
1984b63837 | ||
|
|
36b12d5dea | ||
|
|
3e83c8e2c6 | ||
|
|
1fa8c03dff | ||
|
|
d6de9c266b | ||
|
|
2142c1b4f7 | ||
|
|
cafba6c67a | ||
|
|
6eccebbe39 | ||
|
|
5e9f03c900 | ||
|
|
7374bb02d1 | ||
|
|
d19794ae4f | ||
|
|
108cf75cad | ||
|
|
34cbb6be39 | ||
|
|
907f27baf8 | ||
|
|
4710fa7cee | ||
|
|
bfa1f4d0c6 | ||
|
|
737e0d72b0 | ||
|
|
6edeb3e3b8 | ||
|
|
fb00e98a5b | ||
|
|
4f952eff83 | ||
|
|
1eede97c08 | ||
|
|
95419562ee | ||
|
|
7b85d947c4 | ||
|
|
08e670091a | ||
|
|
1348b632a8 | ||
|
|
8cd5162362 | ||
|
|
1c410a89d8 | ||
|
|
6a931fced4 | ||
|
|
622e2eeb90 | ||
|
|
42cd9fff0c | ||
|
|
2a85a438ba | ||
|
|
338b3b1615 | ||
|
|
363f164f47 | ||
|
|
739f04ece9 | ||
|
|
c6ade68ce2 | ||
|
|
7d60e259e2 | ||
|
|
cd71c6a9c1 | ||
|
|
26fe130609 | ||
|
|
95f87b1b05 | ||
|
|
aebdf3e8df | ||
|
|
5d4fc36256 | ||
|
|
b799d785b9 | ||
|
|
d55442fa53 | ||
|
|
c870b749a4 | ||
|
|
4421cbc0c5 | ||
|
|
b6a3305f23 | ||
|
|
5680260136 | ||
|
|
bc66ea6ecb | ||
|
|
1b20218445 | ||
|
|
466e442353 | ||
|
|
9e6751f47f | ||
|
|
f868923905 | ||
|
|
5d3fd6d939 | ||
|
|
37659d687e | ||
|
|
f23b7e2c2f | ||
|
|
55ccdd63d4 | ||
|
|
5c88890ed4 | ||
|
|
fa0185c9a4 | ||
|
|
e1ed9a8e5e | ||
|
|
9f7e3bc282 | ||
|
|
4ec060ba24 | ||
|
|
b1f1753a8d | ||
|
|
94a62f8c31 | ||
|
|
e6f49f256c | ||
|
|
a048b0aa4a | ||
|
|
b6617c140c | ||
|
|
1fdf012f65 | ||
|
|
8fe0321ac0 | ||
|
|
4a2199fa13 | ||
|
|
69680a53a0 | ||
|
|
785dea6545 | ||
|
|
885932afe1 | ||
|
|
a38ccf3dcc | ||
|
|
d7b1f9cc7e | ||
|
|
6e92097544 | ||
|
|
76403b4ddc | ||
|
|
41ad8d8edb | ||
|
|
68754977b0 | ||
|
|
78ad6d5457 | ||
|
|
cb049ebf41 | ||
|
|
f2f5c3201f | ||
|
|
1ba178eae8 | ||
|
|
838f80c30c | ||
|
|
dc0408881e | ||
|
|
1b5e8cb69c | ||
|
|
9e13a3a467 | ||
|
|
32653c3b8a | ||
|
|
16258a2631 | ||
|
|
4c646249ad | ||
|
|
45c8a8d8a9 | ||
|
|
eff3444834 | ||
|
|
25f68f338c | ||
|
|
7ef363a3c2 | ||
|
|
3065e2bece | ||
|
|
1075d66122 | ||
|
|
d477b33774 | ||
|
|
7b374f8618 | ||
|
|
4869c00c0e | ||
|
|
ff4a5a68d9 | ||
|
|
d77a527b15 | ||
|
|
01caca48dc | ||
|
|
43e545b6ae | ||
|
|
8b65956dcc | ||
|
|
7b522ae120 | ||
|
|
43d134d9ad | ||
|
|
24c787f47d | ||
|
|
f95ac8be8c | ||
|
|
41442012c2 | ||
|
|
6f1195dded | ||
|
|
86e8feaabc | ||
|
|
670ca78a5b | ||
|
|
73ceed0f60 | ||
|
|
8412270772 | ||
|
|
686b47898c | ||
|
|
e03eaf2982 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: mikayla_f
|
||||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -5,12 +5,10 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- latest
|
|
||||||
- devel
|
- devel
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- latest
|
|
||||||
- devel
|
- devel
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
|
|||||||
18
.github/workflows/manifest.yml
vendored
18
.github/workflows/manifest.yml
vendored
@@ -6,7 +6,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- latest
|
|
||||||
- devel
|
- devel
|
||||||
|
|
||||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
@@ -43,7 +42,7 @@ jobs:
|
|||||||
- name: Create outputs folders
|
- name: Create outputs folders
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
shell: bash
|
shell: bash
|
||||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel
|
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
|
||||||
- name: Generate manifest and shields for main branch
|
- name: Generate manifest and shields for main branch
|
||||||
id: manifest-main
|
id: manifest-main
|
||||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||||
@@ -51,21 +50,6 @@ jobs:
|
|||||||
- name: Save main's manifest
|
- name: Save main's manifest
|
||||||
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
||||||
run: mv install_manifest.json deploy/manifests/main
|
run: mv install_manifest.json deploy/manifests/main
|
||||||
# Generate manifest for latest branch
|
|
||||||
- name: Checkout latest
|
|
||||||
id: checkout-latest
|
|
||||||
if: success() || failure()
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: 'latest'
|
|
||||||
clean: false
|
|
||||||
- name: Generate manifest for latest
|
|
||||||
id: manifest-latest
|
|
||||||
if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }}
|
|
||||||
run: python imgen.py
|
|
||||||
- name: Save latest's manifest
|
|
||||||
if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }}
|
|
||||||
run: mv install_manifest.json deploy/manifests/latest
|
|
||||||
# Generate manifest for devel branch
|
# Generate manifest for devel branch
|
||||||
- name: Checkout devel
|
- name: Checkout devel
|
||||||
id: checkout-devel
|
id: checkout-devel
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright © 2022 - 2023 Mikayla Fischler
|
Copyright © 2022 - 2024 Mikayla Fischler
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ Mod Requirements:
|
|||||||
|
|
||||||
Mod Recommendations:
|
Mod Recommendations:
|
||||||
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
|
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
|
||||||
|
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
|
||||||
|
|
||||||
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1
|
v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
145
ccmsi.lua
145
ccmsi.lua
@@ -1,7 +1,7 @@
|
|||||||
--[[
|
--[[
|
||||||
CC-MEK-SCADA Installer Utility
|
CC-MEK-SCADA Installer Utility
|
||||||
|
|
||||||
Copyright (c) 2023 Mikayla Fischler
|
Copyright (c) 2023 - 2024 Mikayla Fischler
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
@@ -18,7 +18,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||||||
local function println(message) print(tostring(message)) end
|
local function println(message) print(tostring(message)) end
|
||||||
local function print(message) term.write(tostring(message)) end
|
local function print(message) term.write(tostring(message)) end
|
||||||
|
|
||||||
local CCMSI_VERSION = "v1.11a"
|
local CCMSI_VERSION = "v1.14"
|
||||||
|
|
||||||
local install_dir = "/.install-cache"
|
local install_dir = "/.install-cache"
|
||||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||||
@@ -26,12 +26,13 @@ local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada
|
|||||||
|
|
||||||
local opts = { ... }
|
local opts = { ... }
|
||||||
local mode, app, target
|
local mode, app, target
|
||||||
local install_manifest = manifest_path .. "main/install_manifest.json"
|
local install_manifest = manifest_path.."main/install_manifest.json"
|
||||||
|
|
||||||
local function red() term.setTextColor(colors.red) end
|
local function red() term.setTextColor(colors.red) end
|
||||||
local function orange() term.setTextColor(colors.orange) end
|
local function orange() term.setTextColor(colors.orange) end
|
||||||
local function yellow() term.setTextColor(colors.yellow) end
|
local function yellow() term.setTextColor(colors.yellow) end
|
||||||
local function green() term.setTextColor(colors.green) end
|
local function green() term.setTextColor(colors.green) end
|
||||||
|
local function cyan() term.setTextColor(colors.cyan) end
|
||||||
local function blue() term.setTextColor(colors.blue) end
|
local function blue() term.setTextColor(colors.blue) end
|
||||||
local function white() term.setTextColor(colors.white) end
|
local function white() term.setTextColor(colors.white) end
|
||||||
local function lgray() term.setTextColor(colors.lightGray) end
|
local function lgray() term.setTextColor(colors.lightGray) end
|
||||||
@@ -58,17 +59,17 @@ local function ask_y_n(question, default)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- print out a white + blue text message
|
-- print out a white + blue text message
|
||||||
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
|
local function pkg_message(message, package) white();print(message.." ");blue();println(package);white() end
|
||||||
|
|
||||||
-- indicate actions to be taken based on package differences for installs/updates
|
-- indicate actions to be taken based on package differences for installs/updates
|
||||||
local function show_pkg_change(name, v)
|
local function show_pkg_change(name, v)
|
||||||
if v.v_local ~= nil then
|
if v.v_local ~= nil then
|
||||||
if v.v_local ~= v.v_remote then
|
if v.v_local ~= v.v_remote then
|
||||||
print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
|
print("["..name.."] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
|
||||||
elseif mode == "install" then
|
elseif mode == "install" then
|
||||||
pkg_message("[" .. name .. "] reinstalling", v.v_local)
|
pkg_message("["..name.."] reinstalling", v.v_local)
|
||||||
end
|
end
|
||||||
else pkg_message("[" .. name .. "] new install of", v.v_remote) end
|
else pkg_message("["..name.."] new install of", v.v_remote) end
|
||||||
return v.v_local ~= v.v_remote
|
return v.v_local ~= v.v_remote
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ local function get_remote_manifest()
|
|||||||
local response, error = http.get(install_manifest)
|
local response, error = http.get(install_manifest)
|
||||||
if response == nil then
|
if response == nil then
|
||||||
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
|
orange();println("Failed to get installation manifest from GitHub, cannot update or install.")
|
||||||
red();println("HTTP error: " .. error);white()
|
red();println("HTTP error: "..error);white()
|
||||||
return false, {}
|
return false, {}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -154,57 +155,55 @@ local function _clean_dir(dir, tree)
|
|||||||
if tree == nil then tree = {} end
|
if tree == nil then tree = {} end
|
||||||
local ls = fs.list(dir)
|
local ls = fs.list(dir)
|
||||||
for _, val in pairs(ls) do
|
for _, val in pairs(ls) do
|
||||||
local path = dir .. "/" .. val
|
local path = dir.."/"..val
|
||||||
if fs.isDir(path) then
|
if fs.isDir(path) then
|
||||||
_clean_dir(path, tree[val])
|
_clean_dir(path, tree[val])
|
||||||
if #fs.list(path) == 0 then fs.delete(path);println("deleted " .. path) end
|
if #fs.list(path) == 0 then fs.delete(path);println("deleted "..path) end
|
||||||
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then ---@fixme remove condition after migration to settings files
|
elseif (not _in_array(val, tree)) and (val ~= "config.lua" ) then
|
||||||
fs.delete(path)
|
fs.delete(path)
|
||||||
println("deleted " .. path)
|
println("deleted "..path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- go through app/common directories to delete unused files
|
-- go through app/common directories to delete unused files
|
||||||
local function clean(manifest)
|
local function clean(manifest)
|
||||||
local root_ext = false
|
|
||||||
local tree = gen_tree(manifest)
|
local tree = gen_tree(manifest)
|
||||||
|
|
||||||
table.insert(tree, "install_manifest.json")
|
table.insert(tree, "install_manifest.json")
|
||||||
table.insert(tree, "ccmsi.lua")
|
table.insert(tree, "ccmsi.lua")
|
||||||
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
|
table.insert(tree, "log.txt") ---@fixme fix after migration to settings files?
|
||||||
|
|
||||||
lgray()
|
|
||||||
|
|
||||||
local ls = fs.list("/")
|
local ls = fs.list("/")
|
||||||
for _, val in pairs(ls) do
|
for _, val in pairs(ls) do
|
||||||
if fs.isDir(val) then
|
if fs.isDriveRoot(val) then
|
||||||
if tree[val] ~= nil then _clean_dir("/" .. val, tree[val]) end
|
yellow();println("skipped mount '"..val.."'")
|
||||||
if #fs.list(val) == 0 then fs.delete(val);println("deleted " .. val) end
|
elseif fs.isDir(val) then
|
||||||
|
if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val])
|
||||||
|
else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end
|
||||||
|
if #fs.list(val) == 0 then fs.delete(val);lgray();println("deleted empty directory '"..val.."'") end
|
||||||
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
|
elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
|
||||||
root_ext = true
|
white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();println("deleted "..val) end
|
||||||
yellow();println(val .. " not used")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
white()
|
white()
|
||||||
if root_ext then println("Files in root directory won't be automatically deleted.") end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- get and validate command line options
|
-- get and validate command line options
|
||||||
|
|
||||||
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --")
|
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --")
|
||||||
|
|
||||||
if #opts == 0 or opts[1] == "help" then
|
if #opts == 0 or opts[1] == "help" then
|
||||||
println("usage: ccmsi <mode> <app> <branch>")
|
println("usage: ccmsi <mode> <app> <branch>")
|
||||||
println("<mode>")
|
println("<mode>")
|
||||||
lgray()
|
lgray()
|
||||||
println(" check - check latest versions avilable")
|
println(" check - check latest versions available")
|
||||||
yellow()
|
yellow()
|
||||||
println(" ccmsi check <branch> for target")
|
println(" ccmsi check <branch> for target")
|
||||||
lgray()
|
lgray()
|
||||||
println(" install - fresh install, overwrites config.lua")
|
println(" install - fresh install")
|
||||||
println(" update - update files EXCEPT for config.lua")
|
println(" update - update files")
|
||||||
println(" uninstall - delete files INCLUDING config/logs")
|
println(" uninstall - delete files INCLUDING config/logs")
|
||||||
white();println("<app>");lgray()
|
white();println("<app>");lgray()
|
||||||
println(" reactor-plc - reactor PLC firmware")
|
println(" reactor-plc - reactor PLC firmware")
|
||||||
@@ -214,7 +213,7 @@ if #opts == 0 or opts[1] == "help" then
|
|||||||
println(" pocket - pocket application")
|
println(" pocket - pocket application")
|
||||||
println(" installer - ccmsi installer (update only)")
|
println(" installer - ccmsi installer (update only)")
|
||||||
white();println("<branch>")
|
white();println("<branch>")
|
||||||
lgray();println(" main (default) | latest | devel");white()
|
lgray();println(" main (default) | devel");white()
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||||
@@ -234,14 +233,14 @@ else
|
|||||||
|
|
||||||
-- determine target
|
-- determine target
|
||||||
if mode == "check" then target = opts[2] else target = opts[3] end
|
if mode == "check" then target = opts[2] else target = opts[3] end
|
||||||
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
|
if (target ~= "main") and (target ~= "devel") then
|
||||||
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
|
if (target and target ~= "") then yellow();println("Unknown target, defaulting to 'main'");white() end
|
||||||
target = "main"
|
target = "main"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set paths
|
-- set paths
|
||||||
install_manifest = manifest_path .. target .. "/install_manifest.json"
|
install_manifest = manifest_path..target.."/install_manifest.json"
|
||||||
repo_path = repo_path .. target .. "/"
|
repo_path = repo_path..target.."/"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- run selected mode
|
-- run selected mode
|
||||||
@@ -261,23 +260,21 @@ if mode == "check" then
|
|||||||
-- list all versions
|
-- list all versions
|
||||||
for key, value in pairs(manifest.versions) do
|
for key, value in pairs(manifest.versions) do
|
||||||
term.setTextColor(colors.purple)
|
term.setTextColor(colors.purple)
|
||||||
print(string.format("%-14s", "[" .. key .. "]"))
|
print(string.format("%-14s", "["..key.."]"))
|
||||||
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
|
||||||
blue();print(local_manifest.versions[key])
|
blue();print(local_manifest.versions[key])
|
||||||
if value ~= local_manifest.versions[key] then
|
if value ~= local_manifest.versions[key] then
|
||||||
white();print(" (")
|
white();print(" (")
|
||||||
term.setTextColor(colors.cyan)
|
cyan();print(value);white();println(" available)")
|
||||||
print(value);white();println(" available)")
|
|
||||||
else green();println(" (up to date)") end
|
else green();println(" (up to date)") end
|
||||||
else
|
else
|
||||||
lgray();print("not installed");white();print(" (latest ")
|
lgray();print("not installed");white();print(" (latest ")
|
||||||
term.setTextColor(colors.cyan)
|
cyan();print(value);white();println(")")
|
||||||
print(value);white();println(")")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if manifest.versions.installer ~= local_manifest.versions.installer then
|
if manifest.versions.installer ~= local_manifest.versions.installer then
|
||||||
yellow();println("\nA newer version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
|
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
|
||||||
end
|
end
|
||||||
elseif mode == "install" or mode == "update" then
|
elseif mode == "install" or mode == "update" then
|
||||||
local update_installer = app == "installer"
|
local update_installer = app == "installer"
|
||||||
@@ -315,13 +312,13 @@ elseif mode == "install" or mode == "update" then
|
|||||||
end
|
end
|
||||||
|
|
||||||
if manifest.versions.installer ~= CCMSI_VERSION then
|
if manifest.versions.installer ~= CCMSI_VERSION then
|
||||||
if not update_installer then yellow();println("A newer version of the installer is available, it is recommended to update to it.");white() end
|
if not update_installer then yellow();println("A different version of the installer is available, it is recommended to update to it.");white() end
|
||||||
if update_installer or ask_y_n("Would you like to update now") then
|
if update_installer or ask_y_n("Would you like to update now") then
|
||||||
lgray();println("GET ccmsi.lua")
|
lgray();println("GET ccmsi.lua")
|
||||||
local dl, err = http.get(repo_path .. "ccmsi.lua")
|
local dl, err = http.get(repo_path.."ccmsi.lua")
|
||||||
|
|
||||||
if dl == nil then
|
if dl == nil then
|
||||||
red();println("HTTP Error " .. err)
|
red();println("HTTP Error "..err)
|
||||||
println("Installer download failed.");white()
|
println("Installer download failed.");white()
|
||||||
else
|
else
|
||||||
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
|
local handle = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file, regardless of name or location
|
||||||
@@ -345,12 +342,8 @@ elseif mode == "install" or mode == "update" then
|
|||||||
ver.lockbox.v_remote = manifest.versions.lockbox
|
ver.lockbox.v_remote = manifest.versions.lockbox
|
||||||
|
|
||||||
green()
|
green()
|
||||||
if mode == "install" then
|
if mode == "install" then print("Installing ") else print("Updating ") end
|
||||||
println("Installing " .. app .. " files...")
|
println(app.." files...");white()
|
||||||
elseif mode == "update" then
|
|
||||||
println("Updating " .. app .. " files... (keeping old config.lua)")
|
|
||||||
end
|
|
||||||
white()
|
|
||||||
|
|
||||||
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
|
ver.boot.changed = show_pkg_change("bootldr", ver.boot)
|
||||||
ver.common.changed = show_pkg_change("common", ver.common)
|
ver.common.changed = show_pkg_change("common", ver.common)
|
||||||
@@ -376,7 +369,6 @@ elseif mode == "install" or mode == "update" then
|
|||||||
local file_list = manifest.files
|
local file_list = manifest.files
|
||||||
local size_list = manifest.sizes
|
local size_list = manifest.sizes
|
||||||
local dependencies = manifest.depends[app]
|
local dependencies = manifest.depends[app]
|
||||||
local config_file = app .. "/config.lua"
|
|
||||||
|
|
||||||
table.insert(dependencies, app)
|
table.insert(dependencies, app)
|
||||||
|
|
||||||
@@ -423,15 +415,15 @@ elseif mode == "install" or mode == "update" then
|
|||||||
|
|
||||||
local files = file_list[dependency]
|
local files = file_list[dependency]
|
||||||
for _, file in pairs(files) do
|
for _, file in pairs(files) do
|
||||||
println("GET " .. file)
|
println("GET "..file)
|
||||||
local dl, err = http.get(repo_path .. file)
|
local dl, err = http.get(repo_path..file)
|
||||||
|
|
||||||
if dl == nil then
|
if dl == nil then
|
||||||
red();println("HTTP Error " .. err)
|
red();println("HTTP Error "..err)
|
||||||
success = false
|
success = false
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
local handle = fs.open(install_dir .. "/" .. file, "w")
|
local handle = fs.open(install_dir.."/"..file, "w")
|
||||||
handle.write(dl.readAll())
|
handle.write(dl.readAll())
|
||||||
handle.close()
|
handle.close()
|
||||||
end
|
end
|
||||||
@@ -450,11 +442,9 @@ elseif mode == "install" or mode == "update" then
|
|||||||
|
|
||||||
local files = file_list[dependency]
|
local files = file_list[dependency]
|
||||||
for _, file in pairs(files) do
|
for _, file in pairs(files) do
|
||||||
if mode == "install" or file ~= config_file then
|
local temp_file = install_dir.."/"..file
|
||||||
local temp_file = install_dir .. "/" .. file
|
if fs.exists(file) then fs.delete(file) end
|
||||||
if fs.exists(file) then fs.delete(file) end
|
fs.move(temp_file, file)
|
||||||
fs.move(temp_file, file)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -487,19 +477,17 @@ elseif mode == "install" or mode == "update" then
|
|||||||
|
|
||||||
local files = file_list[dependency]
|
local files = file_list[dependency]
|
||||||
for _, file in pairs(files) do
|
for _, file in pairs(files) do
|
||||||
if mode == "install" or file ~= config_file then
|
println("GET "..file)
|
||||||
println("GET " .. file)
|
local dl, err = http.get(repo_path..file)
|
||||||
local dl, err = http.get(repo_path .. file)
|
|
||||||
|
|
||||||
if dl == nil then
|
if dl == nil then
|
||||||
red();println("HTTP Error " .. err)
|
red();println("HTTP Error "..err)
|
||||||
success = false
|
success = false
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
local handle = fs.open("/" .. file, "w")
|
local handle = fs.open("/"..file, "w")
|
||||||
handle.write(dl.readAll())
|
handle.write(dl.readAll())
|
||||||
handle.close()
|
handle.close()
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -529,11 +517,11 @@ elseif mode == "uninstall" then
|
|||||||
end
|
end
|
||||||
|
|
||||||
if manifest.versions[app] == nil then
|
if manifest.versions[app] == nil then
|
||||||
red();println("Error: '" .. app .. "' is not installed.")
|
red();println("Error: '"..app.."' is not installed.")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
orange();println("Uninstalling all " .. app .. " files...")
|
orange();println("Uninstalling all "..app.." files...")
|
||||||
|
|
||||||
-- ask for confirmation
|
-- ask for confirmation
|
||||||
if not ask_y_n("Continue", false) then return end
|
if not ask_y_n("Continue", false) then return end
|
||||||
@@ -548,16 +536,16 @@ elseif mode == "uninstall" then
|
|||||||
|
|
||||||
-- delete log file
|
-- delete log file
|
||||||
local log_deleted = false
|
local log_deleted = false
|
||||||
local settings_file = app .. ".settings"
|
local settings_file = app..".settings"
|
||||||
local legacy_config_file = app .. "/config.lua"
|
local legacy_config_file = app.."/config.lua"
|
||||||
|
|
||||||
lgray()
|
lgray()
|
||||||
if fs.exists(legacy_config_file) then
|
if fs.exists(legacy_config_file) then
|
||||||
log_deleted = pcall(function ()
|
log_deleted = pcall(function ()
|
||||||
local config = require(app .. ".config")
|
local config = require(app..".config")
|
||||||
if fs.exists(config.LOG_PATH) then
|
if fs.exists(config.LOG_PATH) then
|
||||||
fs.delete(config.LOG_PATH)
|
fs.delete(config.LOG_PATH)
|
||||||
println("deleted log file " .. config.LOG_PATH)
|
println("deleted log file "..config.LOG_PATH)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
elseif fs.exists(settings_file) and settings.load(settings_file) then
|
elseif fs.exists(settings_file) and settings.load(settings_file) then
|
||||||
@@ -565,7 +553,7 @@ elseif mode == "uninstall" then
|
|||||||
if log ~= nil and fs.exists(log) then
|
if log ~= nil and fs.exists(log) then
|
||||||
log_deleted = true
|
log_deleted = true
|
||||||
fs.delete(log)
|
fs.delete(log)
|
||||||
println("deleted log file " .. log)
|
println("deleted log file "..log)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -579,7 +567,7 @@ elseif mode == "uninstall" then
|
|||||||
for _, dependency in pairs(dependencies) do
|
for _, dependency in pairs(dependencies) do
|
||||||
local files = file_list[dependency]
|
local files = file_list[dependency]
|
||||||
for _, file in pairs(files) do
|
for _, file in pairs(files) do
|
||||||
if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
|
if fs.exists(file) then fs.delete(file);println("deleted "..file) end
|
||||||
end
|
end
|
||||||
|
|
||||||
local folder = files[1]
|
local folder = files[1]
|
||||||
@@ -590,13 +578,16 @@ elseif mode == "uninstall" then
|
|||||||
|
|
||||||
if fs.isDir(folder) then
|
if fs.isDir(folder) then
|
||||||
fs.delete(folder)
|
fs.delete(folder)
|
||||||
println("deleted directory " .. folder)
|
println("deleted directory "..folder)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if fs.exists(legacy_config_file) then
|
||||||
|
fs.delete(legacy_config_file);println("deleted "..legacy_config_file)
|
||||||
|
end
|
||||||
|
|
||||||
if fs.exists(settings_file) then
|
if fs.exists(settings_file) then
|
||||||
fs.delete(settings_file)
|
fs.delete(settings_file);println("deleted "..settings_file)
|
||||||
println("deleted " .. settings_file)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
fs.delete("install_manifest.json")
|
fs.delete("install_manifest.json")
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
||||||
|
|
||||||
if fs.exists("reactor-plc/configure.lua") then
|
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
|
||||||
require("reactor-plc.configure").configure()
|
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
|
||||||
elseif fs.exists("rtu/startup.lua") then
|
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
|
||||||
print("CONFIGURE> RTU CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
|
||||||
elseif fs.exists("supervisor/startup.lua") then
|
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
|
||||||
print("CONFIGURE> SUPERVISOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
|
||||||
elseif fs.exists("coordinator/startup.lua") then
|
|
||||||
print("CONFIGURE> COORDINATOR CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
|
||||||
elseif fs.exists("pocket/startup.lua") then
|
|
||||||
print("CONFIGURE> POCKET CONFIGURATOR NOT YET IMPLEMENTED IN BETA")
|
|
||||||
else
|
else
|
||||||
print("CONFIGURE> NO CONFIGURATOR FOUND")
|
print("CONFIGURE> NO CONFIGURATOR FOUND")
|
||||||
print("CONFIGURE> EXIT")
|
print("CONFIGURE> EXIT")
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
local config = {}
|
|
||||||
|
|
||||||
-- supervisor comms channel
|
|
||||||
config.SVR_CHANNEL = 16240
|
|
||||||
-- coordinator comms channel
|
|
||||||
config.CRD_CHANNEL = 16243
|
|
||||||
-- pocket comms channel
|
|
||||||
config.PKT_CHANNEL = 16244
|
|
||||||
-- max trusted modem message distance (0 to disable check)
|
|
||||||
config.TRUSTED_RANGE = 0
|
|
||||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
|
||||||
config.SV_TIMEOUT = 5
|
|
||||||
config.API_TIMEOUT = 5
|
|
||||||
-- facility authentication key (do NOT use one of your passwords)
|
|
||||||
-- this enables verifying that messages are authentic
|
|
||||||
-- all devices on the same network must use the same key
|
|
||||||
-- config.AUTH_KEY = "SCADAfacility123"
|
|
||||||
|
|
||||||
-- expected number of reactor units, used only to require that number of unit monitors
|
|
||||||
config.NUM_UNITS = 4
|
|
||||||
|
|
||||||
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
|
|
||||||
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
|
|
||||||
config.SOUNDER_VOLUME = 1.0
|
|
||||||
|
|
||||||
-- true for 24 hour time on main view screen
|
|
||||||
config.TIME_24_HOUR = true
|
|
||||||
|
|
||||||
-- disable flow view (for legacy layouts)
|
|
||||||
config.DISABLE_FLOW_VIEW = false
|
|
||||||
|
|
||||||
-- log path
|
|
||||||
config.LOG_PATH = "/log.txt"
|
|
||||||
-- log mode
|
|
||||||
-- 0 = APPEND (adds to existing file on start)
|
|
||||||
-- 1 = NEW (replaces existing file on start)
|
|
||||||
config.LOG_MODE = 0
|
|
||||||
-- true to log verbose debug messages
|
|
||||||
config.LOG_DEBUG = false
|
|
||||||
|
|
||||||
return config
|
|
||||||
1312
coordinator/configure.lua
Normal file
1312
coordinator/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,6 @@ local process = require("coordinator.process")
|
|||||||
|
|
||||||
local apisessions = require("coordinator.session.apisessions")
|
local apisessions = require("coordinator.session.apisessions")
|
||||||
|
|
||||||
local dialog = require("coordinator.ui.dialog")
|
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
local println = util.println
|
|
||||||
|
|
||||||
local PROTOCOL = comms.PROTOCOL
|
local PROTOCOL = comms.PROTOCOL
|
||||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||||
@@ -26,32 +21,75 @@ local LINK_TIMEOUT = 60.0
|
|||||||
|
|
||||||
local coordinator = {}
|
local coordinator = {}
|
||||||
|
|
||||||
-- request the user to select a monitor
|
---@type crd_config
|
||||||
---@nodiscard
|
local config = {}
|
||||||
---@param names table available monitors
|
|
||||||
---@return boolean|string|nil
|
|
||||||
local function ask_monitor(names)
|
|
||||||
println("available monitors:")
|
|
||||||
for i = 1, #names do
|
|
||||||
print(" " .. names[i])
|
|
||||||
end
|
|
||||||
println("")
|
|
||||||
println("select a monitor or type c to cancel")
|
|
||||||
|
|
||||||
local iface = dialog.ask_options(names, "c")
|
coordinator.config = config
|
||||||
|
|
||||||
if iface ~= false and iface ~= nil then
|
-- load the coordinator configuration<br>
|
||||||
util.filter_table(names, function (x) return x ~= iface end)
|
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
|
||||||
|
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
|
||||||
|
function coordinator.load_config()
|
||||||
|
if not settings.load("/coordinator.settings") then return 1 end
|
||||||
|
|
||||||
|
config.UnitCount = settings.get("UnitCount")
|
||||||
|
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||||
|
config.Time24Hour = settings.get("Time24Hour")
|
||||||
|
|
||||||
|
config.DisableFlowView = settings.get("DisableFlowView")
|
||||||
|
config.MainDisplay = settings.get("MainDisplay")
|
||||||
|
config.FlowDisplay = settings.get("FlowDisplay")
|
||||||
|
config.UnitDisplays = settings.get("UnitDisplays")
|
||||||
|
|
||||||
|
config.SVR_Channel = settings.get("SVR_Channel")
|
||||||
|
config.CRD_Channel = settings.get("CRD_Channel")
|
||||||
|
config.PKT_Channel = settings.get("PKT_Channel")
|
||||||
|
config.SVR_Timeout = settings.get("SVR_Timeout")
|
||||||
|
config.API_Timeout = settings.get("API_Timeout")
|
||||||
|
config.TrustedRange = settings.get("TrustedRange")
|
||||||
|
config.AuthKey = settings.get("AuthKey")
|
||||||
|
|
||||||
|
config.LogMode = settings.get("LogMode")
|
||||||
|
config.LogPath = settings.get("LogPath")
|
||||||
|
config.LogDebug = settings.get("LogDebug")
|
||||||
|
|
||||||
|
local cfv = util.new_validator()
|
||||||
|
|
||||||
|
cfv.assert_type_int(config.UnitCount)
|
||||||
|
cfv.assert_range(config.UnitCount, 1, 4)
|
||||||
|
cfv.assert_type_bool(config.Time24Hour)
|
||||||
|
|
||||||
|
cfv.assert_type_bool(config.DisableFlowView)
|
||||||
|
cfv.assert_type_table(config.UnitDisplays)
|
||||||
|
|
||||||
|
cfv.assert_type_num(config.SpeakerVolume)
|
||||||
|
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||||
|
|
||||||
|
cfv.assert_channel(config.SVR_Channel)
|
||||||
|
cfv.assert_channel(config.CRD_Channel)
|
||||||
|
cfv.assert_channel(config.PKT_Channel)
|
||||||
|
|
||||||
|
cfv.assert_type_num(config.SVR_Timeout)
|
||||||
|
cfv.assert_min(config.SVR_Timeout, 2)
|
||||||
|
cfv.assert_type_num(config.API_Timeout)
|
||||||
|
cfv.assert_min(config.API_Timeout, 2)
|
||||||
|
|
||||||
|
cfv.assert_type_num(config.TrustedRange)
|
||||||
|
cfv.assert_min(config.TrustedRange, 0)
|
||||||
|
cfv.assert_type_str(config.AuthKey)
|
||||||
|
|
||||||
|
if type(config.AuthKey) == "string" then
|
||||||
|
local len = string.len(config.AuthKey)
|
||||||
|
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
return iface
|
cfv.assert_type_int(config.LogMode)
|
||||||
end
|
cfv.assert_range(config.LogMode, 0, 1)
|
||||||
|
cfv.assert_type_str(config.LogPath)
|
||||||
|
cfv.assert_type_bool(config.LogDebug)
|
||||||
|
|
||||||
|
-- Monitor Setup
|
||||||
|
|
||||||
-- configure monitor layout
|
|
||||||
---@param num_units integer number of units expected
|
|
||||||
---@param disable_flow_view boolean disable flow view (legacy)
|
|
||||||
---@return boolean success, monitors_struct? monitors
|
|
||||||
function coordinator.configure_monitors(num_units, disable_flow_view)
|
|
||||||
---@class monitors_struct
|
---@class monitors_struct
|
||||||
local monitors = {
|
local monitors = {
|
||||||
primary = nil, ---@type table|nil
|
primary = nil, ---@type table|nil
|
||||||
@@ -62,146 +100,73 @@ function coordinator.configure_monitors(num_units, disable_flow_view)
|
|||||||
unit_name_map = {}
|
unit_name_map = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
local monitors_avail = ppm.get_monitor_list()
|
local mon_cfv = util.new_validator()
|
||||||
local names = {}
|
|
||||||
local available = {}
|
|
||||||
|
|
||||||
-- get all interface names
|
-- get all interface names
|
||||||
for iface, _ in pairs(monitors_avail) do
|
local names = {}
|
||||||
table.insert(names, iface)
|
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
|
||||||
table.insert(available, iface)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- we need a certain number of monitors (1 per unit + 1 primary display + 1 flow display)
|
local function setup_monitors()
|
||||||
local num_displays_needed = num_units + util.trinary(disable_flow_view, 1, 2)
|
mon_cfv.assert_type_str(config.MainDisplay)
|
||||||
if #names < num_displays_needed then
|
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
|
||||||
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
|
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
|
||||||
println(message)
|
|
||||||
log.warning(message)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- attempt to load settings
|
if mon_cfv.valid() then
|
||||||
if not settings.load("/coord.settings") then
|
local w, h, _
|
||||||
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
|
|
||||||
else
|
|
||||||
local _primary = settings.get("PRIMARY_DISPLAY")
|
|
||||||
local _flow = settings.get("FLOW_DISPLAY")
|
|
||||||
local _unitd = settings.get("UNIT_DISPLAYS")
|
|
||||||
|
|
||||||
-- filter out already assigned monitors
|
if not util.table_contains(names, config.MainDisplay) then
|
||||||
util.filter_table(available, function (x) return x ~= _primary end)
|
return 2, "Main monitor is not connected."
|
||||||
util.filter_table(available, function (x) return x ~= _flow end)
|
|
||||||
if type(_unitd) == "table" then
|
|
||||||
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---------------------
|
|
||||||
-- PRIMARY DISPLAY --
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
|
|
||||||
|
|
||||||
if not util.table_contains(names, iface_primary_display) then
|
|
||||||
println("primary display is not connected")
|
|
||||||
local response = dialog.ask_y_n("would you like to change it", true)
|
|
||||||
if response == false then return false end
|
|
||||||
iface_primary_display = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
while iface_primary_display == nil and #available > 0 do
|
|
||||||
iface_primary_display = ask_monitor(available)
|
|
||||||
end
|
|
||||||
|
|
||||||
if type(iface_primary_display) ~= "string" then return false end
|
|
||||||
|
|
||||||
settings.set("PRIMARY_DISPLAY", iface_primary_display)
|
|
||||||
util.filter_table(available, function (x) return x ~= iface_primary_display end)
|
|
||||||
|
|
||||||
monitors.primary = ppm.get_periph(iface_primary_display)
|
|
||||||
monitors.primary_name = iface_primary_display
|
|
||||||
|
|
||||||
--------------------------
|
|
||||||
-- FLOW MONITOR DISPLAY --
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
if not disable_flow_view then
|
|
||||||
local iface_flow_display = settings.get("FLOW_DISPLAY") ---@type boolean|string|nil
|
|
||||||
|
|
||||||
if not util.table_contains(names, iface_flow_display) then
|
|
||||||
println("flow monitor display is not connected")
|
|
||||||
local response = dialog.ask_y_n("would you like to change it", true)
|
|
||||||
if response == false then return false end
|
|
||||||
iface_flow_display = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
while iface_flow_display == nil and #available > 0 do
|
|
||||||
iface_flow_display = ask_monitor(available)
|
|
||||||
end
|
|
||||||
|
|
||||||
if type(iface_flow_display) ~= "string" then return false end
|
|
||||||
|
|
||||||
settings.set("FLOW_DISPLAY", iface_flow_display)
|
|
||||||
util.filter_table(available, function (x) return x ~= iface_flow_display end)
|
|
||||||
|
|
||||||
monitors.flow = ppm.get_periph(iface_flow_display)
|
|
||||||
monitors.flow_name = iface_flow_display
|
|
||||||
end
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
-- UNIT DISPLAYS --
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
local unit_displays = settings.get("UNIT_DISPLAYS")
|
|
||||||
|
|
||||||
if unit_displays == nil then
|
|
||||||
unit_displays = {}
|
|
||||||
for i = 1, num_units do
|
|
||||||
local display = nil
|
|
||||||
|
|
||||||
while display == nil and #available > 0 do
|
|
||||||
println("please select monitor for unit #" .. i)
|
|
||||||
display = ask_monitor(available)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if display == false then return false end
|
monitors.primary = ppm.get_periph(config.MainDisplay)
|
||||||
|
monitors.primary_name = config.MainDisplay
|
||||||
|
|
||||||
unit_displays[i] = display
|
monitors.primary.setTextScale(0.5)
|
||||||
end
|
w, _ = ppm.monitor_block_size(monitors.primary.getSize())
|
||||||
else
|
if w ~= 8 then
|
||||||
-- make sure all displays are connected
|
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
|
||||||
for i = 1, num_units do
|
|
||||||
local display = unit_displays[i]
|
|
||||||
|
|
||||||
if not util.table_contains(names, display) then
|
|
||||||
println("unit #" .. i .. " display is not connected")
|
|
||||||
local response = dialog.ask_y_n("would you like to change it", true)
|
|
||||||
if response == false then return false end
|
|
||||||
display = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
while display == nil and #available > 0 do
|
if not config.DisableFlowView then
|
||||||
display = ask_monitor(available)
|
if not util.table_contains(names, config.FlowDisplay) then
|
||||||
|
return 2, "Flow monitor is not connected."
|
||||||
|
end
|
||||||
|
|
||||||
|
monitors.flow = ppm.get_periph(config.FlowDisplay)
|
||||||
|
monitors.flow_name = config.FlowDisplay
|
||||||
|
|
||||||
|
monitors.flow.setTextScale(0.5)
|
||||||
|
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
|
||||||
|
if w ~= 8 then
|
||||||
|
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if display == false then return false end
|
for i = 1, config.UnitCount do
|
||||||
|
local display = config.UnitDisplays[i]
|
||||||
|
if type(display) ~= "string" or not util.table_contains(names, display) then
|
||||||
|
return 2, "Unit " .. i .. " monitor is not connected."
|
||||||
|
end
|
||||||
|
|
||||||
unit_displays[i] = display
|
monitors.unit_displays[i] = ppm.get_periph(display)
|
||||||
end
|
monitors.unit_name_map[i] = display
|
||||||
|
|
||||||
|
monitors.unit_displays[i].setTextScale(0.5)
|
||||||
|
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
|
||||||
|
if w ~= 4 or h ~= 4 then
|
||||||
|
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else return 2, "Monitor configuration invalid." end
|
||||||
end
|
end
|
||||||
|
|
||||||
settings.set("UNIT_DISPLAYS", unit_displays)
|
if cfv.valid() then
|
||||||
if not settings.save("/coord.settings") then
|
local ok, result, message = pcall(setup_monitors)
|
||||||
log.warning("configure_monitors(): failed to save coordinator settings file")
|
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
|
||||||
end
|
if result == 2 then return 2, message end
|
||||||
|
else return 1 end
|
||||||
|
|
||||||
for i = 1, #unit_displays do
|
return 0, monitors
|
||||||
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
|
|
||||||
monitors.unit_name_map[i] = unit_displays[i]
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, monitors
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- dmesg print wrapper
|
-- dmesg print wrapper
|
||||||
@@ -246,13 +211,8 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param version string coordinator version
|
---@param version string coordinator version
|
||||||
---@param nic nic network interface device
|
---@param nic nic network interface device
|
||||||
---@param num_units integer number of configured units for number of monitors, checked against SV
|
|
||||||
---@param crd_channel integer port of configured supervisor
|
|
||||||
---@param svr_channel integer listening port for supervisor replys
|
|
||||||
---@param pkt_channel integer listening port for pocket API
|
|
||||||
---@param range integer trusted device connection range
|
|
||||||
---@param sv_watchdog watchdog
|
---@param sv_watchdog watchdog
|
||||||
function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pkt_channel, range, sv_watchdog)
|
function coordinator.comms(version, nic, sv_watchdog)
|
||||||
local self = {
|
local self = {
|
||||||
sv_linked = false,
|
sv_linked = false,
|
||||||
sv_addr = comms.BROADCAST,
|
sv_addr = comms.BROADCAST,
|
||||||
@@ -267,16 +227,16 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
est_task_done = nil
|
est_task_done = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
comms.set_trusted_range(range)
|
comms.set_trusted_range(config.TrustedRange)
|
||||||
|
|
||||||
-- PRIVATE FUNCTIONS --
|
|
||||||
|
|
||||||
-- configure network channels
|
-- configure network channels
|
||||||
nic.closeAll()
|
nic.closeAll()
|
||||||
nic.open(crd_channel)
|
nic.open(config.CRD_Channel)
|
||||||
|
|
||||||
-- link nic to apisessions
|
-- pass config to apisessions
|
||||||
apisessions.init(nic)
|
apisessions.init(nic, config)
|
||||||
|
|
||||||
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
-- send a packet to the supervisor
|
-- send a packet to the supervisor
|
||||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||||
@@ -296,7 +256,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
pkt.make(msg_type, msg)
|
pkt.make(msg_type, msg)
|
||||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||||
|
|
||||||
nic.transmit(svr_channel, crd_channel, s_pkt)
|
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
|
||||||
self.sv_seq_num = self.sv_seq_num + 1
|
self.sv_seq_num = self.sv_seq_num + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -310,7 +270,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
|
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
|
||||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||||
|
|
||||||
nic.transmit(pkt_channel, crd_channel, s_pkt)
|
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
|
||||||
self.last_api_est_acks[packet.src_addr()] = ack
|
self.last_api_est_acks[packet.src_addr()] = ack
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -343,7 +303,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
self.est_last = self.est_start
|
self.est_last = self.est_start
|
||||||
|
|
||||||
self.est_tick_waiting, self.est_task_done =
|
self.est_tick_waiting, self.est_task_done =
|
||||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. svr_channel)
|
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
|
||||||
|
|
||||||
_send_establish()
|
_send_establish()
|
||||||
else
|
else
|
||||||
@@ -356,7 +316,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
if abort then
|
if abort then
|
||||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||||
elseif self.sv_config_err then
|
elseif self.sv_config_err then
|
||||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||||
elseif not self.sv_linked then
|
elseif not self.sv_linked then
|
||||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||||
coordinator.log_comms("supervisor connection attempt denied")
|
coordinator.log_comms("supervisor connection attempt denied")
|
||||||
@@ -371,7 +331,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
|
|
||||||
ok = false
|
ok = false
|
||||||
elseif self.sv_config_err then
|
elseif self.sv_config_err then
|
||||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||||
ok = false
|
ok = false
|
||||||
elseif (util.time_s() - self.est_last) > 1.0 then
|
elseif (util.time_s() - self.est_last) > 1.0 then
|
||||||
_send_establish()
|
_send_establish()
|
||||||
@@ -405,10 +365,10 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- send the auto process control configuration with a start command
|
-- send the auto process control configuration with a start command
|
||||||
---@param config coord_auto_config configuration
|
---@param auto_cfg coord_auto_config configuration
|
||||||
function public.send_auto_start(config)
|
function public.send_auto_start(auto_cfg)
|
||||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
||||||
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -464,9 +424,9 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
local src_addr = packet.scada_frame.src_addr()
|
local src_addr = packet.scada_frame.src_addr()
|
||||||
local protocol = packet.scada_frame.protocol()
|
local protocol = packet.scada_frame.protocol()
|
||||||
|
|
||||||
if l_chan ~= crd_channel then
|
if l_chan ~= config.CRD_Channel then
|
||||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||||
elseif r_chan == pkt_channel then
|
elseif r_chan == config.PKT_Channel then
|
||||||
if not self.sv_linked then
|
if not self.sv_linked then
|
||||||
log.debug("discarding pocket API packet before linked to supervisor")
|
log.debug("discarding pocket API packet before linked to supervisor")
|
||||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||||
@@ -526,7 +486,7 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
else
|
else
|
||||||
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
||||||
end
|
end
|
||||||
elseif r_chan == svr_channel then
|
elseif r_chan == config.SVR_Channel then
|
||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.sv_r_seq_num == nil then
|
if self.sv_r_seq_num == nil then
|
||||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||||
@@ -699,22 +659,22 @@ function coordinator.comms(version, nic, num_units, crd_channel, svr_channel, pk
|
|||||||
-- connection with supervisor established
|
-- connection with supervisor established
|
||||||
if packet.length == 2 then
|
if packet.length == 2 then
|
||||||
local est_ack = packet.data[1]
|
local est_ack = packet.data[1]
|
||||||
local config = packet.data[2]
|
local sv_config = packet.data[2]
|
||||||
|
|
||||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||||
-- reset to disconnected before validating
|
-- reset to disconnected before validating
|
||||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||||
|
|
||||||
if type(config) == "table" and #config == 2 then
|
if type(sv_config) == "table" and #sv_config == 2 then
|
||||||
-- get configuration
|
-- get configuration
|
||||||
|
|
||||||
---@class facility_conf
|
---@class facility_conf
|
||||||
local conf = {
|
local conf = {
|
||||||
num_units = config[1], ---@type integer
|
num_units = sv_config[1], ---@type integer
|
||||||
cooling = config[2] ---@type sv_cooling_conf
|
cooling = sv_config[2] ---@type sv_cooling_conf
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.num_units == num_units then
|
if conf.num_units == config.UnitCount then
|
||||||
-- init io controller
|
-- init io controller
|
||||||
iocontrol.init(conf, public)
|
iocontrol.init(conf, public)
|
||||||
|
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ function iocontrol.init(conf, comms)
|
|||||||
-- determine tank information
|
-- determine tank information
|
||||||
if io.facility.tank_mode == 0 then
|
if io.facility.tank_mode == 0 then
|
||||||
io.facility.tank_defs = {}
|
io.facility.tank_defs = {}
|
||||||
-- on facility tank mode 0, setup tank defs to match unit TANK option
|
-- on facility tank mode 0, setup tank defs to match unit tank option
|
||||||
for i = 1, conf.num_units do
|
for i = 1, conf.num_units do
|
||||||
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TANK, 1, 0)
|
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
|
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
|
||||||
@@ -214,7 +214,7 @@ function iocontrol.init(conf, comms)
|
|||||||
num_boilers = 0,
|
num_boilers = 0,
|
||||||
num_turbines = 0,
|
num_turbines = 0,
|
||||||
num_snas = 0,
|
num_snas = 0,
|
||||||
has_tank = conf.cooling.r_cool[i].TANK,
|
has_tank = conf.cooling.r_cool[i].TankConnection,
|
||||||
|
|
||||||
control_state = false,
|
control_state = false,
|
||||||
burn_rate_cmd = 0.0,
|
burn_rate_cmd = 0.0,
|
||||||
@@ -295,13 +295,13 @@ function iocontrol.init(conf, comms)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- create boiler tables
|
-- create boiler tables
|
||||||
for _ = 1, conf.cooling.r_cool[i].BOILERS do
|
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
|
||||||
table.insert(entry.boiler_ps_tbl, psil.create())
|
table.insert(entry.boiler_ps_tbl, psil.create())
|
||||||
table.insert(entry.boiler_data_tbl, {})
|
table.insert(entry.boiler_data_tbl, {})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create turbine tables
|
-- create turbine tables
|
||||||
for _ = 1, conf.cooling.r_cool[i].TURBINES do
|
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
|
||||||
table.insert(entry.turbine_ps_tbl, psil.create())
|
table.insert(entry.turbine_ps_tbl, psil.create())
|
||||||
table.insert(entry.turbine_data_tbl, {})
|
table.insert(entry.turbine_data_tbl, {})
|
||||||
end
|
end
|
||||||
@@ -469,7 +469,7 @@ function iocontrol.record_unit_builds(builds)
|
|||||||
|
|
||||||
-- note: if not all units and RTUs are connected, some will be nil
|
-- note: if not all units and RTUs are connected, some will be nil
|
||||||
for id, build in pairs(builds) do
|
for id, build in pairs(builds) do
|
||||||
local unit = io.units[id] ---@type ioctl_unit
|
local unit = io.units[id] ---@type ioctl_unit
|
||||||
|
|
||||||
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
|
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
|
||||||
|
|
||||||
@@ -694,8 +694,8 @@ function iocontrol.update_facility_status(status)
|
|||||||
|
|
||||||
for id, sps in pairs(rtu_statuses.sps) do
|
for id, sps in pairs(rtu_statuses.sps) do
|
||||||
if type(fac.sps_data_tbl[id]) == "table" then
|
if type(fac.sps_data_tbl[id]) == "table" then
|
||||||
local data = fac.sps_data_tbl[id] ---@type sps_session_db
|
local data = fac.sps_data_tbl[id] ---@type sps_session_db
|
||||||
local ps = fac.sps_ps_tbl[id] ---@type psil
|
local ps = fac.sps_ps_tbl[id] ---@type psil
|
||||||
|
|
||||||
local rtu_faulted = _record_multiblock_status(sps, data, ps)
|
local rtu_faulted = _record_multiblock_status(sps, data, ps)
|
||||||
|
|
||||||
@@ -732,8 +732,8 @@ function iocontrol.update_facility_status(status)
|
|||||||
|
|
||||||
for id, tank in pairs(rtu_statuses.tanks) do
|
for id, tank in pairs(rtu_statuses.tanks) do
|
||||||
if type(fac.tank_data_tbl[id]) == "table" then
|
if type(fac.tank_data_tbl[id]) == "table" then
|
||||||
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
|
local data = fac.tank_data_tbl[id] ---@type dynamicv_session_db
|
||||||
local ps = fac.tank_ps_tbl[id] ---@type psil
|
local ps = fac.tank_ps_tbl[id] ---@type psil
|
||||||
|
|
||||||
local rtu_faulted = _record_multiblock_status(tank, data, ps)
|
local rtu_faulted = _record_multiblock_status(tank, data, ps)
|
||||||
|
|
||||||
@@ -760,20 +760,34 @@ function iocontrol.update_facility_status(status)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- environment detector status
|
-- environment detector status
|
||||||
if type(rtu_statuses.rad_mon) == "table" then
|
if type(rtu_statuses.envds) == "table" then
|
||||||
if #rtu_statuses.rad_mon > 0 then
|
local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
|
||||||
local rad_mon = rtu_statuses.rad_mon[1]
|
|
||||||
local rtu_faulted = rad_mon[1] ---@type boolean
|
|
||||||
fac.radiation = rad_mon[2] ---@type number
|
|
||||||
|
|
||||||
fac.ps.publish("rad_computed_status", util.trinary(rtu_faulted, 2, 3))
|
for _, envd in pairs(rtu_statuses.envds) do
|
||||||
fac.ps.publish("radiation", fac.radiation)
|
local rtu_faulted = envd[1] ---@type boolean
|
||||||
|
local radiation = envd[2] ---@type radiation_reading
|
||||||
|
local rad_raw = envd[3] ---@type number
|
||||||
|
|
||||||
|
any_conn = true
|
||||||
|
any_faulted = any_faulted or rtu_faulted
|
||||||
|
|
||||||
|
if rad_raw > max_rad then
|
||||||
|
max_rad = rad_raw
|
||||||
|
max_reading = radiation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if any_conn then
|
||||||
|
fac.radiation = max_reading
|
||||||
|
fac.ps.publish("rad_computed_status", util.trinary(any_faulted, 2, 3))
|
||||||
else
|
else
|
||||||
fac.radiation = types.new_zero_radiation_reading()
|
fac.radiation = types.new_zero_radiation_reading()
|
||||||
fac.ps.publish("rad_computed_status", 1)
|
fac.ps.publish("rad_computed_status", 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
fac.ps.publish("radiation", fac.radiation)
|
||||||
else
|
else
|
||||||
log.debug(log_header .. "radiation monitor list not a table")
|
log.debug(log_header .. "environment detector list not a table")
|
||||||
valid = false
|
valid = false
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -917,8 +931,8 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
|
|
||||||
for id, boiler in pairs(rtu_statuses.boilers) do
|
for id, boiler in pairs(rtu_statuses.boilers) do
|
||||||
if type(unit.boiler_data_tbl[id]) == "table" then
|
if type(unit.boiler_data_tbl[id]) == "table" then
|
||||||
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
|
||||||
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
local ps = unit.boiler_ps_tbl[id] ---@type psil
|
||||||
|
|
||||||
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
|
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
|
||||||
|
|
||||||
@@ -960,8 +974,8 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
|
|
||||||
for id, turbine in pairs(rtu_statuses.turbines) do
|
for id, turbine in pairs(rtu_statuses.turbines) do
|
||||||
if type(unit.turbine_data_tbl[id]) == "table" then
|
if type(unit.turbine_data_tbl[id]) == "table" then
|
||||||
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
|
||||||
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
local ps = unit.turbine_ps_tbl[id] ---@type psil
|
||||||
|
|
||||||
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
|
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
|
||||||
|
|
||||||
@@ -1033,9 +1047,9 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
|
|
||||||
-- solar neutron activator status info
|
-- solar neutron activator status info
|
||||||
if type(rtu_statuses.sna) == "table" then
|
if type(rtu_statuses.sna) == "table" then
|
||||||
unit.num_snas = rtu_statuses.sna[1] ---@type integer
|
unit.num_snas = rtu_statuses.sna[1] ---@type integer
|
||||||
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
|
unit.sna_prod_rate = rtu_statuses.sna[2] ---@type number
|
||||||
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number
|
unit.sna_peak_rate = rtu_statuses.sna[3] ---@type number
|
||||||
|
|
||||||
unit.unit_ps.publish("sna_count", unit.num_snas)
|
unit.unit_ps.publish("sna_count", unit.num_snas)
|
||||||
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
|
unit.unit_ps.publish("sna_prod_rate", unit.sna_prod_rate)
|
||||||
@@ -1048,16 +1062,28 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- environment detector status
|
-- environment detector status
|
||||||
if type(rtu_statuses.rad_mon) == "table" then
|
if type(rtu_statuses.envds) == "table" then
|
||||||
if #rtu_statuses.rad_mon > 0 then
|
local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
|
||||||
local rad_mon = rtu_statuses.rad_mon[1]
|
|
||||||
-- local rtu_faulted = rad_mon[1] ---@type boolean
|
|
||||||
unit.radiation = rad_mon[2] ---@type number
|
|
||||||
|
|
||||||
unit.unit_ps.publish("radiation", unit.radiation)
|
for _, envd in pairs(rtu_statuses.envds) do
|
||||||
|
local radiation = envd[2] ---@type radiation_reading
|
||||||
|
local rad_raw = envd[3] ---@type number
|
||||||
|
|
||||||
|
any_conn = true
|
||||||
|
|
||||||
|
if rad_raw > max_rad then
|
||||||
|
max_rad = rad_raw
|
||||||
|
max_reading = radiation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if any_conn then
|
||||||
|
unit.radiation = max_reading
|
||||||
else
|
else
|
||||||
unit.radiation = types.new_zero_radiation_reading()
|
unit.radiation = types.new_zero_radiation_reading()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unit.unit_ps.publish("radiation", unit.radiation)
|
||||||
else
|
else
|
||||||
log.debug(log_header .. "radiation monitor list not a table")
|
log.debug(log_header .. "radiation monitor list not a table")
|
||||||
valid = false
|
valid = false
|
||||||
|
|||||||
@@ -19,15 +19,20 @@ local process = {}
|
|||||||
local self = {
|
local self = {
|
||||||
io = nil, ---@type ioctl
|
io = nil, ---@type ioctl
|
||||||
comms = nil, ---@type coord_comms
|
comms = nil, ---@type coord_comms
|
||||||
---@class coord_auto_config
|
---@class coord_control_states
|
||||||
config = {
|
control_states = {
|
||||||
mode = PROCESS.INACTIVE,
|
---@class coord_auto_config
|
||||||
burn_target = 0.0,
|
process = {
|
||||||
charge_target = 0.0,
|
mode = PROCESS.INACTIVE,
|
||||||
gen_target = 0.0,
|
burn_target = 0.0,
|
||||||
limits = {},
|
charge_target = 0.0,
|
||||||
waste_product = PRODUCT.PLUTONIUM,
|
gen_target = 0.0,
|
||||||
pu_fallback = false
|
limits = {},
|
||||||
|
waste_product = PRODUCT.PLUTONIUM,
|
||||||
|
pu_fallback = false
|
||||||
|
},
|
||||||
|
waste_modes = {},
|
||||||
|
priority_groups = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,63 +47,64 @@ function process.init(iocontrol, coord_comms)
|
|||||||
self.io = iocontrol
|
self.io = iocontrol
|
||||||
self.comms = coord_comms
|
self.comms = coord_comms
|
||||||
|
|
||||||
|
local ctl_proc = self.control_states.process
|
||||||
|
|
||||||
for i = 1, self.io.facility.num_units do
|
for i = 1, self.io.facility.num_units do
|
||||||
self.config.limits[i] = 0.1
|
ctl_proc.limits[i] = 0.1
|
||||||
end
|
end
|
||||||
|
|
||||||
-- load settings
|
local ctrl_states = settings.get("ControlStates", {})
|
||||||
if not settings.load("/coord.settings") then
|
local config = ctrl_states.process ---@type coord_auto_config
|
||||||
log.error("process.init(): failed to load coordinator settings file")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- facility auto control configuration
|
-- facility auto control configuration
|
||||||
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
|
|
||||||
if type(config) == "table" then
|
if type(config) == "table" then
|
||||||
self.config.mode = config.mode
|
ctl_proc.mode = config.mode
|
||||||
self.config.burn_target = config.burn_target
|
ctl_proc.burn_target = config.burn_target
|
||||||
self.config.charge_target = config.charge_target
|
ctl_proc.charge_target = config.charge_target
|
||||||
self.config.gen_target = config.gen_target
|
ctl_proc.gen_target = config.gen_target
|
||||||
self.config.limits = config.limits
|
ctl_proc.limits = config.limits
|
||||||
self.config.waste_product = config.waste_product
|
ctl_proc.waste_product = config.waste_product
|
||||||
self.config.pu_fallback = config.pu_fallback
|
ctl_proc.pu_fallback = config.pu_fallback
|
||||||
|
|
||||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||||
self.io.facility.ps.publish("process_waste_product", self.config.waste_product)
|
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||||
self.io.facility.ps.publish("process_pu_fallback", self.config.pu_fallback)
|
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||||
|
|
||||||
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
|
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
|
||||||
local unit = self.io.units[id] ---@type ioctl_unit
|
local unit = self.io.units[id] ---@type ioctl_unit
|
||||||
unit.unit_ps.publish("burn_limit", self.config.limits[id])
|
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||||
end
|
end
|
||||||
|
|
||||||
log.info("PROCESS: loaded auto control settings from coord.settings")
|
log.info("PROCESS: loaded auto control settings")
|
||||||
|
|
||||||
-- notify supervisor of auto waste config
|
-- notify supervisor of auto waste config
|
||||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, self.config.waste_product)
|
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, self.config.pu_fallback)
|
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- unit waste states
|
-- unit waste states
|
||||||
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil
|
local waste_modes = ctrl_states.waste_modes ---@type table|nil
|
||||||
if type(waste_modes) == "table" then
|
if type(waste_modes) == "table" then
|
||||||
for id, mode in pairs(waste_modes) do
|
for id, mode in pairs(waste_modes) do
|
||||||
|
self.control_states.waste_modes[id] = mode
|
||||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
log.info("PROCESS: loaded unit waste mode settings from coord.settings")
|
log.info("PROCESS: loaded unit waste mode settings")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- unit priority groups
|
-- unit priority groups
|
||||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
local prio_groups = ctrl_states.priority_groups ---@type table|nil
|
||||||
if type(prio_groups) == "table" then
|
if type(prio_groups) == "table" then
|
||||||
for id, group in pairs(prio_groups) do
|
for id, group in pairs(prio_groups) do
|
||||||
|
self.control_states.priority_groups[id] = group
|
||||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||||
end
|
end
|
||||||
|
|
||||||
log.info("PROCESS: loaded priority groups settings from coord.settings")
|
log.info("PROCESS: loaded priority groups settings")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -155,15 +161,10 @@ function process.set_unit_waste(id, mode)
|
|||||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||||
|
|
||||||
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
|
self.control_states.waste_modes[id] = mode
|
||||||
|
settings.set("ControlStates", self.control_states)
|
||||||
|
|
||||||
if type(waste_mode) ~= "table" then waste_mode = {} end
|
if not settings.save("/coordinator.settings") then
|
||||||
|
|
||||||
waste_mode[id] = mode
|
|
||||||
|
|
||||||
settings.set("WASTE_MODES", waste_mode)
|
|
||||||
|
|
||||||
if not settings.save("/coord.settings") then
|
|
||||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -198,15 +199,10 @@ function process.set_group(unit_id, group_id)
|
|||||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
|
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
|
||||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||||
|
|
||||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
self.control_states.priority_groups[unit_id] = group_id
|
||||||
|
settings.set("ControlStates", self.control_states)
|
||||||
|
|
||||||
if type(prio_groups) ~= "table" then prio_groups = {} end
|
if not settings.save("/coordinator.settings") then
|
||||||
|
|
||||||
prio_groups[unit_id] = group_id
|
|
||||||
|
|
||||||
settings.set("PRIORITY_GROUPS", prio_groups)
|
|
||||||
|
|
||||||
if not settings.save("/coord.settings") then
|
|
||||||
log.error("process.set_group(): failed to save coordinator settings file")
|
log.error("process.set_group(): failed to save coordinator settings file")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -217,20 +213,14 @@ end
|
|||||||
|
|
||||||
-- write auto process control to config file
|
-- write auto process control to config file
|
||||||
local function _write_auto_config()
|
local function _write_auto_config()
|
||||||
-- attempt to load settings
|
|
||||||
if not settings.load("/coord.settings") then
|
|
||||||
log.warning("process._write_auto_config(): failed to load coordinator settings file")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- save config
|
-- save config
|
||||||
settings.set("PROCESS", self.config)
|
settings.set("ControlStates", self.control_states)
|
||||||
local saved = settings.save("/coord.settings")
|
local saved = settings.save("/coordinator.settings")
|
||||||
|
|
||||||
if not saved then
|
if not saved then
|
||||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||||
end
|
end
|
||||||
|
|
||||||
return not not saved
|
return saved
|
||||||
end
|
end
|
||||||
|
|
||||||
-- stop automatic process control
|
-- stop automatic process control
|
||||||
@@ -241,7 +231,7 @@ end
|
|||||||
|
|
||||||
-- start automatic process control
|
-- start automatic process control
|
||||||
function process.start_auto()
|
function process.start_auto()
|
||||||
self.comms.send_auto_start(self.config)
|
self.comms.send_auto_start(self.control_states.process)
|
||||||
log.debug("PROCESS: START AUTO CTL")
|
log.debug("PROCESS: START AUTO CTL")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -253,7 +243,7 @@ function process.set_process_waste(product)
|
|||||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||||
|
|
||||||
-- update config table and save
|
-- update config table and save
|
||||||
self.config.waste_product = product
|
self.control_states.process.waste_product = product
|
||||||
_write_auto_config()
|
_write_auto_config()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -265,7 +255,7 @@ function process.set_pu_fallback(enabled)
|
|||||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||||
|
|
||||||
-- update config table and save
|
-- update config table and save
|
||||||
self.config.pu_fallback = enabled
|
self.control_states.process.pu_fallback = enabled
|
||||||
_write_auto_config()
|
_write_auto_config()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -279,11 +269,12 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
|
|||||||
log.debug("PROCESS: SAVE")
|
log.debug("PROCESS: SAVE")
|
||||||
|
|
||||||
-- update config table
|
-- update config table
|
||||||
self.config.mode = mode
|
local ctl_proc = self.control_states.process
|
||||||
self.config.burn_target = burn_target
|
ctl_proc.mode = mode
|
||||||
self.config.charge_target = charge_target
|
ctl_proc.burn_target = burn_target
|
||||||
self.config.gen_target = gen_target
|
ctl_proc.charge_target = charge_target
|
||||||
self.config.limits = limits
|
ctl_proc.gen_target = gen_target
|
||||||
|
ctl_proc.limits = limits
|
||||||
|
|
||||||
-- save config
|
-- save config
|
||||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||||
@@ -294,22 +285,23 @@ end
|
|||||||
function process.start_ack_handle(response)
|
function process.start_ack_handle(response)
|
||||||
local ack = response[1]
|
local ack = response[1]
|
||||||
|
|
||||||
self.config.mode = response[2]
|
local ctl_proc = self.control_states.process
|
||||||
self.config.burn_target = response[3]
|
ctl_proc.mode = response[2]
|
||||||
self.config.charge_target = response[4]
|
ctl_proc.burn_target = response[3]
|
||||||
self.config.gen_target = response[5]
|
ctl_proc.charge_target = response[4]
|
||||||
|
ctl_proc.gen_target = response[5]
|
||||||
|
|
||||||
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
||||||
self.config.limits[i] = response[6][i]
|
ctl_proc.limits[i] = response[6][i]
|
||||||
|
|
||||||
local unit = self.io.units[i] ---@type ioctl_unit
|
local unit = self.io.units[i] ---@type ioctl_unit
|
||||||
unit.unit_ps.publish("burn_limit", self.config.limits[i])
|
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||||
end
|
end
|
||||||
|
|
||||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||||
|
|
||||||
self.io.facility.start_ack(ack)
|
self.io.facility.start_ack(ack)
|
||||||
end
|
end
|
||||||
@@ -317,14 +309,14 @@ end
|
|||||||
-- record waste product state after attempting to change it
|
-- record waste product state after attempting to change it
|
||||||
---@param response WASTE_PRODUCT supervisor waste product state
|
---@param response WASTE_PRODUCT supervisor waste product state
|
||||||
function process.waste_ack_handle(response)
|
function process.waste_ack_handle(response)
|
||||||
self.config.waste_product = response
|
self.control_states.process.waste_product = response
|
||||||
self.io.facility.ps.publish("process_waste_product", response)
|
self.io.facility.ps.publish("process_waste_product", response)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- record plutonium fallback state after attempting to change it
|
-- record plutonium fallback state after attempting to change it
|
||||||
---@param response boolean supervisor plutonium fallback state
|
---@param response boolean supervisor plutonium fallback state
|
||||||
function process.pu_fb_ack_handle(response)
|
function process.pu_fb_ack_handle(response)
|
||||||
self.config.pu_fallback = response
|
self.control_states.process.pu_fallback = response
|
||||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
--
|
--
|
||||||
|
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local util = require("scada-common.util")
|
|
||||||
|
|
||||||
local iocontrol = require("coordinator.iocontrol")
|
local iocontrol = require("coordinator.iocontrol")
|
||||||
|
|
||||||
@@ -93,39 +92,6 @@ function renderer.init_displays()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check main display width
|
|
||||||
---@nodiscard
|
|
||||||
---@return boolean width_okay
|
|
||||||
function renderer.validate_main_display_width()
|
|
||||||
local w, _ = engine.monitors.primary.getSize()
|
|
||||||
return w == 164
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check flow display width
|
|
||||||
---@nodiscard
|
|
||||||
---@return boolean width_okay
|
|
||||||
function renderer.validate_flow_display_width()
|
|
||||||
local w, _ = engine.monitors.flow.getSize()
|
|
||||||
return w == 164
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check display sizes
|
|
||||||
---@nodiscard
|
|
||||||
---@return boolean valid all unit display dimensions OK
|
|
||||||
function renderer.validate_unit_display_sizes()
|
|
||||||
local valid = true
|
|
||||||
|
|
||||||
for id, monitor in ipairs(engine.monitors.unit_displays) do
|
|
||||||
local w, h = monitor.getSize()
|
|
||||||
if w ~= 79 or h ~= 52 then
|
|
||||||
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
|
|
||||||
valid = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return valid
|
|
||||||
end
|
|
||||||
|
|
||||||
-- initialize the dmesg output window
|
-- initialize the dmesg output window
|
||||||
function renderer.init_dmesg()
|
function renderer.init_dmesg()
|
||||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ local log = require("scada-common.log")
|
|||||||
local mqueue = require("scada-common.mqueue")
|
local mqueue = require("scada-common.mqueue")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("coordinator.config")
|
|
||||||
local iocontrol = require("coordinator.iocontrol")
|
local iocontrol = require("coordinator.iocontrol")
|
||||||
|
|
||||||
local pocket = require("coordinator.session.pocket")
|
local pocket = require("coordinator.session.pocket")
|
||||||
@@ -11,7 +10,8 @@ local pocket = require("coordinator.session.pocket")
|
|||||||
local apisessions = {}
|
local apisessions = {}
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
nic = nil,
|
nic = nil, ---@type nic
|
||||||
|
config = nil, ---@type crd_config
|
||||||
next_id = 0,
|
next_id = 0,
|
||||||
sessions = {}
|
sessions = {}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ local function _api_handle_outq(session)
|
|||||||
if msg ~= nil then
|
if msg ~= nil then
|
||||||
if msg.qtype == mqueue.TYPE.PACKET then
|
if msg.qtype == mqueue.TYPE.PACKET then
|
||||||
-- handle a packet to be sent
|
-- handle a packet to be sent
|
||||||
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
|
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||||
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||||
-- handle instruction/notification
|
-- handle instruction/notification
|
||||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||||
@@ -59,7 +59,7 @@ local function _shutdown(session)
|
|||||||
while session.out_queue.ready() do
|
while session.out_queue.ready() do
|
||||||
local msg = session.out_queue.pop()
|
local msg = session.out_queue.pop()
|
||||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||||
self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message)
|
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -69,9 +69,11 @@ end
|
|||||||
-- PUBLIC FUNCTIONS --
|
-- PUBLIC FUNCTIONS --
|
||||||
|
|
||||||
-- initialize apisessions
|
-- initialize apisessions
|
||||||
---@param nic nic
|
---@param nic nic network interface
|
||||||
function apisessions.init(nic)
|
---@param config crd_config coordinator config
|
||||||
|
function apisessions.init(nic, config)
|
||||||
self.nic = nic
|
self.nic = nic
|
||||||
|
self.config = config
|
||||||
end
|
end
|
||||||
|
|
||||||
-- find a session by remote port
|
-- find a session by remote port
|
||||||
@@ -103,7 +105,7 @@ function apisessions.establish_session(source_addr, version)
|
|||||||
|
|
||||||
local id = self.next_id
|
local id = self.next_id
|
||||||
|
|
||||||
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT)
|
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
|
||||||
table.insert(self.sessions, pkt_s)
|
table.insert(self.sessions, pkt_s)
|
||||||
|
|
||||||
local mt = {
|
local mt = {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ local util = require("scada-common.util")
|
|||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local config = require("coordinator.config")
|
local configure = require("coordinator.configure")
|
||||||
local coordinator = require("coordinator.coordinator")
|
local coordinator = require("coordinator.coordinator")
|
||||||
local iocontrol = require("coordinator.iocontrol")
|
local iocontrol = require("coordinator.iocontrol")
|
||||||
local renderer = require("coordinator.renderer")
|
local renderer = require("coordinator.renderer")
|
||||||
@@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder")
|
|||||||
|
|
||||||
local apisessions = require("coordinator.session.apisessions")
|
local apisessions = require("coordinator.session.apisessions")
|
||||||
|
|
||||||
local COORDINATOR_VERSION = "v1.0.16"
|
local COORDINATOR_VERSION = "v1.2.2"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
@@ -34,32 +34,34 @@ local log_comms = coordinator.log_comms
|
|||||||
local log_crypto = coordinator.log_crypto
|
local log_crypto = coordinator.log_crypto
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- config validation
|
-- get configuration
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
local cfv = util.new_validator()
|
-- mount connected devices (required for monitor setup)
|
||||||
|
ppm.mount_all()
|
||||||
|
|
||||||
cfv.assert_channel(config.SVR_CHANNEL)
|
local loaded, monitors = coordinator.load_config()
|
||||||
cfv.assert_channel(config.CRD_CHANNEL)
|
if loaded ~= 0 then
|
||||||
cfv.assert_channel(config.PKT_CHANNEL)
|
-- try to reconfigure (user action)
|
||||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
local success, error = configure.configure(loaded, monitors)
|
||||||
cfv.assert_type_num(config.SV_TIMEOUT)
|
if success then
|
||||||
cfv.assert_min(config.SV_TIMEOUT, 2)
|
loaded, monitors = coordinator.load_config()
|
||||||
cfv.assert_type_num(config.API_TIMEOUT)
|
assert(loaded == 0, util.trinary(loaded == 1, "failed to load valid configuration", "monitor configuration invalid"))
|
||||||
cfv.assert_min(config.API_TIMEOUT, 2)
|
else
|
||||||
cfv.assert_type_int(config.NUM_UNITS)
|
assert(success, "coordinator configuration error: " .. error)
|
||||||
cfv.assert_type_num(config.SOUNDER_VOLUME)
|
end
|
||||||
cfv.assert_type_bool(config.TIME_24_HOUR)
|
end
|
||||||
cfv.assert_type_str(config.LOG_PATH)
|
|
||||||
cfv.assert_type_int(config.LOG_MODE)
|
|
||||||
|
|
||||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
-- passed checks, good now
|
||||||
|
---@cast monitors monitors_struct
|
||||||
|
|
||||||
|
local config = coordinator.config
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||||
@@ -77,39 +79,16 @@ local function main()
|
|||||||
-- system startup
|
-- system startup
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
-- mount connected devices
|
-- re-mount devices now that logging is ready
|
||||||
ppm.mount_all()
|
ppm.mount_all()
|
||||||
|
|
||||||
-- report versions/init fp PSIL
|
-- report versions/init fp PSIL
|
||||||
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
||||||
|
|
||||||
-- setup monitors
|
|
||||||
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS, config.DISABLE_FLOW_VIEW == true)
|
|
||||||
if not configured or monitors == nil then
|
|
||||||
println("startup> monitor setup failed")
|
|
||||||
log.fatal("monitor configuration failed")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- init renderer
|
-- init renderer
|
||||||
renderer.legacy_disable_flow_view(config.DISABLE_FLOW_VIEW == true)
|
renderer.legacy_disable_flow_view(config.DisableFlowView)
|
||||||
renderer.set_displays(monitors)
|
renderer.set_displays(monitors)
|
||||||
renderer.init_displays()
|
renderer.init_displays()
|
||||||
|
|
||||||
if not renderer.validate_main_display_width() then
|
|
||||||
println("startup> main display must be 8 blocks wide")
|
|
||||||
log.fatal("main display not wide enough")
|
|
||||||
return
|
|
||||||
elseif (config.DISABLE_FLOW_VIEW ~= true) and not renderer.validate_flow_display_width() then
|
|
||||||
println("startup> flow display must be 8 blocks wide")
|
|
||||||
log.fatal("flow display not wide enough")
|
|
||||||
return
|
|
||||||
elseif not renderer.validate_unit_display_sizes() then
|
|
||||||
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
|
|
||||||
log.fatal("unit display dimensions incorrect")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
renderer.init_dmesg()
|
renderer.init_dmesg()
|
||||||
|
|
||||||
-- lets get started!
|
-- lets get started!
|
||||||
@@ -132,7 +111,7 @@ local function main()
|
|||||||
else
|
else
|
||||||
local sounder_start = util.time_ms()
|
local sounder_start = util.time_ms()
|
||||||
log_boot("annunciator alarm speaker connected")
|
log_boot("annunciator alarm speaker connected")
|
||||||
sounder.init(speaker, config.SOUNDER_VOLUME)
|
sounder.init(speaker, config.SpeakerVolume)
|
||||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||||
log_sys("annunciator alarm configured")
|
log_sys("annunciator alarm configured")
|
||||||
iocontrol.fp_has_speaker(true)
|
iocontrol.fp_has_speaker(true)
|
||||||
@@ -143,8 +122,8 @@ local function main()
|
|||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
-- message authentication init
|
-- message authentication init
|
||||||
if type(config.AUTH_KEY) == "string" then
|
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||||
local init_time = network.init_mac(config.AUTH_KEY)
|
local init_time = network.init_mac(config.AuthKey)
|
||||||
log_crypto("HMAC init took " .. init_time .. "ms")
|
log_crypto("HMAC init took " .. init_time .. "ms")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -161,14 +140,13 @@ local function main()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- create connection watchdog
|
-- create connection watchdog
|
||||||
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
|
local conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||||
conn_watchdog.cancel()
|
conn_watchdog.cancel()
|
||||||
log.debug("startup> conn watchdog created")
|
log.debug("startup> conn watchdog created")
|
||||||
|
|
||||||
-- create network interface then setup comms
|
-- create network interface then setup comms
|
||||||
local nic = network.nic(modem)
|
local nic = network.nic(modem)
|
||||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.NUM_UNITS, config.CRD_CHANNEL,
|
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog)
|
||||||
config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog)
|
|
||||||
log.debug("startup> comms init")
|
log.debug("startup> comms init")
|
||||||
log_comms("comms initialized")
|
log_comms("comms initialized")
|
||||||
|
|
||||||
@@ -214,7 +192,7 @@ local function main()
|
|||||||
|
|
||||||
local link_failed = false
|
local link_failed = false
|
||||||
local ui_ok = true
|
local ui_ok = true
|
||||||
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||||
|
|
||||||
-- start clock
|
-- start clock
|
||||||
loop_clock.start()
|
loop_clock.start()
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ local function init(parent, id)
|
|||||||
if unit.num_boilers > 0 then
|
if unit.num_boilers > 0 then
|
||||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||||
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
|
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||||
@@ -273,7 +273,7 @@ local function init(parent, id)
|
|||||||
|
|
||||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||||
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
|
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
|
||||||
|
|
||||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
local completion = require("cc.completion")
|
|
||||||
|
|
||||||
local util = require("scada-common.util")
|
|
||||||
|
|
||||||
local print = util.print
|
|
||||||
|
|
||||||
local dialog = {}
|
|
||||||
|
|
||||||
-- ask the user yes or no
|
|
||||||
---@nodiscard
|
|
||||||
---@param question string
|
|
||||||
---@param default boolean
|
|
||||||
---@return boolean|nil
|
|
||||||
function dialog.ask_y_n(question, default)
|
|
||||||
print(question)
|
|
||||||
|
|
||||||
if default == true then
|
|
||||||
print(" (Y/n)? ")
|
|
||||||
else
|
|
||||||
print(" (y/N)? ")
|
|
||||||
end
|
|
||||||
|
|
||||||
local response = read(nil, nil)
|
|
||||||
|
|
||||||
if response == "" then
|
|
||||||
return default
|
|
||||||
elseif response == "Y" or response == "y" then
|
|
||||||
return true
|
|
||||||
elseif response == "N" or response == "n" then
|
|
||||||
return false
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ask the user for an input within a set of options
|
|
||||||
---@nodiscard
|
|
||||||
---@param options table
|
|
||||||
---@param cancel string
|
|
||||||
---@return boolean|string|nil
|
|
||||||
function dialog.ask_options(options, cancel)
|
|
||||||
print("> ")
|
|
||||||
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
|
|
||||||
|
|
||||||
if response == cancel then return false end
|
|
||||||
|
|
||||||
if util.table_contains(options, response) then
|
|
||||||
return response
|
|
||||||
else return nil end
|
|
||||||
end
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
|||||||
|
|
||||||
local core = {}
|
local core = {}
|
||||||
|
|
||||||
core.version = "2.0.0"
|
core.version = "2.1.1"
|
||||||
|
|
||||||
core.flasher = flasher
|
core.flasher = flasher
|
||||||
core.events = events
|
core.events = events
|
||||||
@@ -115,7 +115,7 @@ end
|
|||||||
|
|
||||||
-- extract the custom element assert message, dropping the path to the element file
|
-- extract the custom element assert message, dropping the path to the element file
|
||||||
function core.extract_assert_msg(msg)
|
function core.extract_assert_msg(msg)
|
||||||
return string.sub(msg, (string.find(msg, "@") + 1) or 1)
|
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Interactive Field Manager
|
-- Interactive Field Manager
|
||||||
@@ -173,7 +173,7 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
|||||||
if e.enabled then
|
if e.enabled then
|
||||||
e.w_set_bkg(fg_bg.bkg)
|
e.w_set_bkg(fg_bg.bkg)
|
||||||
e.w_set_fgd(fg_bg.fgd)
|
e.w_set_fgd(fg_bg.fgd)
|
||||||
else
|
elseif dis_fg_bg ~= nil then
|
||||||
e.w_set_bkg(dis_fg_bg.bkg)
|
e.w_set_bkg(dis_fg_bg.bkg)
|
||||||
e.w_set_fgd(dis_fg_bg.fgd)
|
e.w_set_fgd(dis_fg_bg.fgd)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
p_window = nil, ---@type table
|
p_window = nil, ---@type table
|
||||||
position = events.new_coord_2d(1, 1),
|
position = events.new_coord_2d(1, 1),
|
||||||
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
|
||||||
|
offset_x = 0,
|
||||||
|
offset_y = 0,
|
||||||
next_y = 1, -- next child y coordinate
|
next_y = 1, -- next child y coordinate
|
||||||
next_id = 0, -- next child ID
|
next_id = 0, -- next child ID
|
||||||
subscriptions = {},
|
subscriptions = {},
|
||||||
@@ -105,6 +107,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
value = nil, ---@type any
|
value = nil, ---@type any
|
||||||
window = nil, ---@type table
|
window = nil, ---@type table
|
||||||
content_window = nil, ---@type table|nil
|
content_window = nil, ---@type table|nil
|
||||||
|
mouse_window_shift = { x = 0, y = 0 },
|
||||||
fg_bg = core.cpair(colors.white, colors.black),
|
fg_bg = core.cpair(colors.white, colors.black),
|
||||||
frame = core.gframe(1, 1, 1, 1),
|
frame = core.gframe(1, 1, 1, 1),
|
||||||
children = {},
|
children = {},
|
||||||
@@ -193,6 +196,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
---@param offset_y integer y offset for mouse events
|
---@param offset_y integer y offset for mouse events
|
||||||
---@param next_y integer next line if no y was provided
|
---@param next_y integer next line if no y was provided
|
||||||
function protected.prepare_template(offset_x, offset_y, next_y)
|
function protected.prepare_template(offset_x, offset_y, next_y)
|
||||||
|
-- record offsets in case there is a reposition
|
||||||
|
self.offset_x = offset_x
|
||||||
|
self.offset_y = offset_y
|
||||||
|
|
||||||
-- get frame coordinates/size
|
-- get frame coordinates/size
|
||||||
if args.gframe ~= nil then
|
if args.gframe ~= nil then
|
||||||
protected.frame.x = args.gframe.x
|
protected.frame.x = args.gframe.x
|
||||||
@@ -217,10 +224,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- check frame
|
-- check frame
|
||||||
element.assert(f.x >= 1, "frame x not >= 1", 2)
|
element.assert(f.x >= 1, "frame x not >= 1", 3)
|
||||||
element.assert(f.y >= 1, "frame y not >= 1", 2)
|
element.assert(f.y >= 1, "frame y not >= 1", 3)
|
||||||
element.assert(f.w >= 1, "frame width not >= 1", 2)
|
element.assert(f.w >= 1, "frame width not >= 1", 3)
|
||||||
element.assert(f.h >= 1, "frame height not >= 1", 2)
|
element.assert(f.h >= 1, "frame height not >= 1", 3)
|
||||||
|
|
||||||
-- create window
|
-- create window
|
||||||
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
|
protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
|
||||||
@@ -344,6 +351,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
-- handle this element having been unfocused
|
-- handle this element having been unfocused
|
||||||
function protected.on_unfocused() end
|
function protected.on_unfocused() end
|
||||||
|
|
||||||
|
-- handle this element having had a child focused
|
||||||
|
---@param child graphics_element
|
||||||
|
function protected.on_child_focused(child) end
|
||||||
|
|
||||||
-- handle this element having been shown
|
-- handle this element having been shown
|
||||||
function protected.on_shown() end
|
function protected.on_shown() end
|
||||||
|
|
||||||
@@ -520,6 +531,13 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
else args.parent.__focus_child(child) end
|
else args.parent.__focus_child(child) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- a child was focused, used to make sure it is actually visible to the user in the content frame
|
||||||
|
---@param child graphics_element
|
||||||
|
function public.__child_focused(child)
|
||||||
|
protected.on_child_focused(child)
|
||||||
|
if not self.is_root then args.parent.__child_focused(public) end
|
||||||
|
end
|
||||||
|
|
||||||
-- get a child element
|
-- get a child element
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param id element_id
|
---@param id element_id
|
||||||
@@ -652,6 +670,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
if args.can_focus and protected.enabled and not self.focused then
|
if args.can_focus and protected.enabled and not self.focused then
|
||||||
self.focused = true
|
self.focused = true
|
||||||
protected.on_focused()
|
protected.on_focused()
|
||||||
|
if not self.is_root then args.parent.__child_focused(public) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -666,7 +685,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
-- unfocus this element and all its children
|
-- unfocus this element and all its children
|
||||||
function public.unfocus_all()
|
function public.unfocus_all()
|
||||||
public.unfocus()
|
public.unfocus()
|
||||||
for _, child in pairs(protected.children) do child.get().unfocus() end
|
for _, child in pairs(protected.children) do child.get().unfocus_all() end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- custom recolor command, varies by element if implemented
|
-- custom recolor command, varies by element if implemented
|
||||||
@@ -681,7 +700,22 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
|
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
|
||||||
---@param x integer x position relative to parent frame
|
---@param x integer x position relative to parent frame
|
||||||
---@param y integer y position relative to parent frame
|
---@param y integer y position relative to parent frame
|
||||||
function public.reposition(x, y) protected.window.reposition(x, y) end
|
function public.reposition(x, y)
|
||||||
|
protected.window.reposition(x, y)
|
||||||
|
|
||||||
|
-- record position
|
||||||
|
self.position.x, self.position.y = protected.window.getPosition()
|
||||||
|
|
||||||
|
-- shift per parent child offset
|
||||||
|
self.position.x = self.position.x + self.offset_x
|
||||||
|
self.position.y = self.position.y + self.offset_y
|
||||||
|
|
||||||
|
-- calculate mouse event bounds
|
||||||
|
self.bounds.x1 = self.position.x
|
||||||
|
self.bounds.x2 = self.position.x + protected.frame.w - 1
|
||||||
|
self.bounds.y1 = self.position.y
|
||||||
|
self.bounds.y2 = self.position.y + protected.frame.h - 1
|
||||||
|
end
|
||||||
|
|
||||||
-- FUNCTION CALLBACKS --
|
-- FUNCTION CALLBACKS --
|
||||||
|
|
||||||
@@ -704,10 +738,11 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
|
local event_T = events.mouse_transposed(event, self.position.x, self.position.y)
|
||||||
|
|
||||||
-- handle the mouse event then pass to children
|
|
||||||
protected.handle_mouse(event_T)
|
protected.handle_mouse(event_T)
|
||||||
for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end
|
|
||||||
|
-- shift child event if the content window has moved then pass to children
|
||||||
|
local c_event_T = events.mouse_transposed(event_T, protected.mouse_window_shift.x + 1, protected.mouse_window_shift.y + 1)
|
||||||
|
for _, child in pairs(protected.children) do child.get().handle_mouse(c_event_T) end
|
||||||
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
|
elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
|
||||||
-- clicked out, unfocus this element and children
|
-- clicked out, unfocus this element and children
|
||||||
public.unfocus_all()
|
public.unfocus_all()
|
||||||
@@ -808,9 +843,12 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
|
|
||||||
-- re-draw this element and all its children
|
-- re-draw this element and all its children
|
||||||
function public.redraw()
|
function public.redraw()
|
||||||
|
local bg, fg = protected.window.getBackgroundColor(), protected.window.getTextColor()
|
||||||
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
protected.window.setBackgroundColor(protected.fg_bg.bkg)
|
||||||
protected.window.setTextColor(protected.fg_bg.fgd)
|
protected.window.setTextColor(protected.fg_bg.fgd)
|
||||||
protected.window.clear()
|
protected.window.clear()
|
||||||
|
protected.window.setBackgroundColor(bg)
|
||||||
|
protected.window.setTextColor(fg)
|
||||||
protected.redraw()
|
protected.redraw()
|
||||||
for _, child in pairs(protected.children) do child.get().redraw() end
|
for _, child in pairs(protected.children) do child.get().redraw() end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ local function checkbox(args)
|
|||||||
|
|
||||||
args.can_focus = true
|
args.can_focus = true
|
||||||
args.height = 1
|
args.height = 1
|
||||||
args.width = 3 + string.len(args.label)
|
args.width = 2 + string.len(args.label)
|
||||||
|
|
||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
local e = element.new(args)
|
local e = element.new(args)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ local tcd = require("scada-common.tcd")
|
|||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
|
local ALIGN = core.ALIGN
|
||||||
|
|
||||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||||
local KEY_CLICK = core.events.KEY_CLICK
|
local KEY_CLICK = core.events.KEY_CLICK
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ local KEY_CLICK = core.events.KEY_CLICK
|
|||||||
---@field text string button text
|
---@field text string button text
|
||||||
---@field callback function function to call on touch
|
---@field callback function function to call on touch
|
||||||
---@field min_width? integer text length if omitted
|
---@field min_width? integer text length if omitted
|
||||||
|
---@field alignment? ALIGN text align if min width > length
|
||||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||||
---@field parent graphics_element
|
---@field parent graphics_element
|
||||||
@@ -31,6 +34,7 @@ local function push_button(args)
|
|||||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||||
|
|
||||||
local text_width = string.len(args.text)
|
local text_width = string.len(args.text)
|
||||||
|
local alignment = args.alignment or ALIGN.CENTER
|
||||||
|
|
||||||
-- set automatic settings
|
-- set automatic settings
|
||||||
args.can_focus = true
|
args.can_focus = true
|
||||||
@@ -41,9 +45,15 @@ local function push_button(args)
|
|||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
local e = element.new(args)
|
local e = element.new(args)
|
||||||
|
|
||||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
local h_pad = 1
|
||||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||||
|
|
||||||
|
if alignment == ALIGN.CENTER then
|
||||||
|
h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||||
|
elseif alignment == ALIGN.RIGHT then
|
||||||
|
h_pad = (e.frame.w - text_width) + 1
|
||||||
|
end
|
||||||
|
|
||||||
-- draw the button
|
-- draw the button
|
||||||
function e.redraw()
|
function e.redraw()
|
||||||
e.window.clear()
|
e.window.clear()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
-- Numeric Value Entry Graphics Element
|
-- Numeric Value Entry Graphics Element
|
||||||
|
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
@@ -8,9 +10,11 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
|||||||
|
|
||||||
---@class number_field_args
|
---@class number_field_args
|
||||||
---@field default? number default value, defaults to 0
|
---@field default? number default value, defaults to 0
|
||||||
---@field min? number minimum, forced on unfocus
|
---@field min? number minimum, enforced on unfocus
|
||||||
---@field max? number maximum, forced on unfocus
|
---@field max? number maximum, enforced on unfocus
|
||||||
---@field max_digits? integer maximum number of digits, defaults to width
|
---@field max_chars? integer maximum number of characters, defaults to width
|
||||||
|
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
|
||||||
|
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
|
||||||
---@field allow_decimal? boolean true to allow decimals
|
---@field allow_decimal? boolean true to allow decimals
|
||||||
---@field allow_negative? boolean true to allow negative numbers
|
---@field allow_negative? boolean true to allow negative numbers
|
||||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||||
@@ -26,6 +30,9 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
|||||||
---@param args number_field_args
|
---@param args number_field_args
|
||||||
---@return graphics_element element, element_id id
|
---@return graphics_element element, element_id id
|
||||||
local function number_field(args)
|
local function number_field(args)
|
||||||
|
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
|
||||||
|
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
|
||||||
|
|
||||||
args.height = 1
|
args.height = 1
|
||||||
args.can_focus = true
|
args.can_focus = true
|
||||||
|
|
||||||
@@ -34,13 +41,13 @@ local function number_field(args)
|
|||||||
|
|
||||||
local has_decimal = false
|
local has_decimal = false
|
||||||
|
|
||||||
args.max_digits = args.max_digits or e.frame.w
|
args.max_chars = args.max_chars or e.frame.w
|
||||||
|
|
||||||
-- set initial value
|
-- set initial value
|
||||||
e.value = "" .. (args.default or 0)
|
e.value = "" .. (args.default or 0)
|
||||||
|
|
||||||
-- make an interactive field manager
|
-- make an interactive field manager
|
||||||
local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg)
|
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg)
|
||||||
|
|
||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
@@ -50,7 +57,7 @@ local function number_field(args)
|
|||||||
if core.events.was_clicked(event.type) then
|
if core.events.was_clicked(event.type) then
|
||||||
e.take_focus()
|
e.take_focus()
|
||||||
|
|
||||||
if event.type == MOUSE_CLICK.UP then
|
if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||||
ifield.move_cursor(event.current.x)
|
ifield.move_cursor(event.current.x)
|
||||||
end
|
end
|
||||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||||
@@ -62,7 +69,7 @@ local function number_field(args)
|
|||||||
-- handle keyboard interaction
|
-- handle keyboard interaction
|
||||||
---@param event key_interaction key event
|
---@param event key_interaction key event
|
||||||
function e.handle_key(event)
|
function e.handle_key(event)
|
||||||
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then
|
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
|
||||||
if tonumber(event.name) then
|
if tonumber(event.name) then
|
||||||
if e.value == 0 then e.value = "" end
|
if e.value == 0 then e.value = "" end
|
||||||
ifield.try_insert_char(event.name)
|
ifield.try_insert_char(event.name)
|
||||||
@@ -127,12 +134,46 @@ local function number_field(args)
|
|||||||
local min = tonumber(args.min)
|
local min = tonumber(args.min)
|
||||||
|
|
||||||
if type(val) == "number" then
|
if type(val) == "number" then
|
||||||
|
if args.max_int_digits or args.max_frac_digits then
|
||||||
|
local str = e.value
|
||||||
|
local ceil = false
|
||||||
|
|
||||||
|
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||||
|
local parts = util.strtok(str, ".")
|
||||||
|
|
||||||
|
if parts[1] and args.max_int_digits then
|
||||||
|
if string.len(parts[1]) > args.max_int_digits then
|
||||||
|
parts[1] = string.rep("9", args.max_int_digits)
|
||||||
|
ceil = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if args.allow_decimal and args.max_frac_digits then
|
||||||
|
if ceil then
|
||||||
|
parts[2] = string.rep("9", args.max_frac_digits)
|
||||||
|
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||||
|
-- add a half of the highest precision fractional value in order to round using floor
|
||||||
|
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||||
|
local value = math.floor(scaled + 0.5)
|
||||||
|
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||||
|
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||||
|
|
||||||
|
val = tonumber((parts[1] or "") .. parts[2])
|
||||||
|
end
|
||||||
|
|
||||||
if type(args.max) == "number" and val > max then
|
if type(args.max) == "number" and val > max then
|
||||||
e.value = "" .. max
|
e.value = "" .. max
|
||||||
ifield.nav_start()
|
ifield.nav_start()
|
||||||
elseif type(args.min) == "number" and val < min then
|
elseif type(args.min) == "number" and val < min then
|
||||||
e.value = "" .. min
|
e.value = "" .. min
|
||||||
ifield.nav_start()
|
ifield.nav_start()
|
||||||
|
else
|
||||||
|
e.value = "" .. val
|
||||||
|
ifield.nav_end()
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
e.value = ""
|
e.value = ""
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ local function text_field(args)
|
|||||||
if core.events.was_clicked(event.type) then
|
if core.events.was_clicked(event.type) then
|
||||||
e.take_focus()
|
e.take_focus()
|
||||||
|
|
||||||
if event.type == MOUSE_CLICK.UP then
|
if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||||
ifield.move_cursor(event.current.x)
|
ifield.move_cursor(event.current.x)
|
||||||
end
|
end
|
||||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ local tcd = require("scada-common.tcd")
|
|||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
local element = require("graphics.element")
|
local element = require("graphics.element")
|
||||||
|
|
||||||
|
local KEY_CLICK = core.events.KEY_CLICK
|
||||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||||
|
|
||||||
---@class listbox_args
|
---@class listbox_args
|
||||||
@@ -33,6 +34,8 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
|
|||||||
---@param args listbox_args
|
---@param args listbox_args
|
||||||
---@return graphics_element element, element_id id
|
---@return graphics_element element, element_id id
|
||||||
local function listbox(args)
|
local function listbox(args)
|
||||||
|
args.can_focus = true
|
||||||
|
|
||||||
-- create new graphics element base object
|
-- create new graphics element base object
|
||||||
local e = element.new(args)
|
local e = element.new(args)
|
||||||
|
|
||||||
@@ -128,7 +131,7 @@ local function listbox(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
e.w_set_cur(e.frame.w, i)
|
e.w_set_cur(e.frame.w, i)
|
||||||
e.w_write(" ")
|
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
||||||
end
|
end
|
||||||
|
|
||||||
e.w_set_bkg(e.fg_bg.bkg)
|
e.w_set_bkg(e.fg_bg.bkg)
|
||||||
@@ -158,6 +161,9 @@ local function listbox(args)
|
|||||||
scroll_frame.reposition(1, 1 + scroll_offset)
|
scroll_frame.reposition(1, 1 + scroll_offset)
|
||||||
scroll_frame.setVisible(true)
|
scroll_frame.setVisible(true)
|
||||||
|
|
||||||
|
-- shift mouse events
|
||||||
|
e.mouse_window_shift.y = scroll_offset
|
||||||
|
|
||||||
draw_bar()
|
draw_bar()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -219,6 +225,32 @@ local function listbox(args)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- handle focus
|
||||||
|
e.on_focused = draw_bar
|
||||||
|
e.on_unfocused = draw_bar
|
||||||
|
|
||||||
|
-- handle a child in the list being focused, make sure it is visible
|
||||||
|
function e.on_child_focused(child)
|
||||||
|
for i = 1, #list do
|
||||||
|
local item = list[i] ---@type listbox_item
|
||||||
|
if item.e == child then
|
||||||
|
if (item.y + scroll_offset) <= 0 then
|
||||||
|
scroll_offset = 1 - item.y
|
||||||
|
update_positions()
|
||||||
|
draw_bar()
|
||||||
|
elseif (item.y + scroll_offset) == 1 then
|
||||||
|
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
|
||||||
|
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
|
||||||
|
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
|
||||||
|
update_positions()
|
||||||
|
draw_bar()
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- handle mouse interaction
|
-- handle mouse interaction
|
||||||
---@param event mouse_interaction mouse event
|
---@param event mouse_interaction mouse event
|
||||||
function e.handle_mouse(event)
|
function e.handle_mouse(event)
|
||||||
@@ -226,23 +258,27 @@ local function listbox(args)
|
|||||||
if event.type == MOUSE_CLICK.TAP then
|
if event.type == MOUSE_CLICK.TAP then
|
||||||
if event.current.x == e.frame.w then
|
if event.current.x == e.frame.w then
|
||||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||||
draw_arrows(1)
|
|
||||||
scroll_up()
|
scroll_up()
|
||||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
if event.current.y == 1 then
|
||||||
|
draw_arrows(1)
|
||||||
|
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||||
|
end
|
||||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||||
draw_arrows(-1)
|
|
||||||
scroll_down()
|
scroll_down()
|
||||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
if event.current.y == e.frame.h then
|
||||||
|
draw_arrows(-1)
|
||||||
|
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif event.type == MOUSE_CLICK.DOWN then
|
elseif event.type == MOUSE_CLICK.DOWN then
|
||||||
if event.current.x == e.frame.w then
|
if event.current.x == e.frame.w then
|
||||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||||
draw_arrows(1)
|
|
||||||
scroll_up()
|
scroll_up()
|
||||||
|
if event.current.y == 1 then draw_arrows(1) end
|
||||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||||
draw_arrows(-1)
|
|
||||||
scroll_down()
|
scroll_down()
|
||||||
|
if event.current.y == e.frame.h then draw_arrows(-1) end
|
||||||
else
|
else
|
||||||
-- clicked on bar
|
-- clicked on bar
|
||||||
holding_bar = true
|
holding_bar = true
|
||||||
@@ -274,6 +310,24 @@ local function listbox(args)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- handle keyboard interaction
|
||||||
|
---@param event key_interaction key event
|
||||||
|
function e.handle_key(event)
|
||||||
|
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||||
|
if event.key == keys.up then
|
||||||
|
scroll_up()
|
||||||
|
elseif event.key == keys.down then
|
||||||
|
scroll_down()
|
||||||
|
elseif event.key == keys.home then
|
||||||
|
scroll_offset = 0
|
||||||
|
update_positions()
|
||||||
|
elseif event.key == keys["end"] then
|
||||||
|
scroll_offset = max_down_scroll
|
||||||
|
update_positions()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- element redraw
|
-- element redraw
|
||||||
function e.redraw()
|
function e.redraw()
|
||||||
draw_arrows(0)
|
draw_arrows(0)
|
||||||
|
|||||||
2
imgen.py
2
imgen.py
@@ -60,7 +60,7 @@ def make_manifest(size):
|
|||||||
},
|
},
|
||||||
"files" : {
|
"files" : {
|
||||||
# common files
|
# common files
|
||||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua" ],
|
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||||
"common" : list_files("./scada-common"),
|
"common" : list_files("./scada-common"),
|
||||||
"graphics" : list_files("./graphics"),
|
"graphics" : list_files("./graphics"),
|
||||||
"lockbox" : list_files("./lockbox"),
|
"lockbox" : list_files("./lockbox"),
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ return {
|
|||||||
-- initialize booted environment
|
-- initialize booted environment
|
||||||
init_env = function ()
|
init_env = function ()
|
||||||
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
|
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
|
||||||
-- overwrite require/package globals
|
|
||||||
require, package = _require.make(_env, "/")
|
require, package = _require.make(_env, "/")
|
||||||
-- reset terminal
|
|
||||||
term.clear(); term.setCursorPos(1, 1)
|
term.clear(); term.setCursorPos(1, 1)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
local config = {}
|
|
||||||
|
|
||||||
-- supervisor comms channel
|
|
||||||
config.SVR_CHANNEL = 16240
|
|
||||||
-- coordinator comms channel
|
|
||||||
config.CRD_CHANNEL = 16243
|
|
||||||
-- pocket comms channel
|
|
||||||
config.PKT_CHANNEL = 16244
|
|
||||||
-- max trusted modem message distance (0 to disable check)
|
|
||||||
config.TRUSTED_RANGE = 0
|
|
||||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
|
||||||
config.COMMS_TIMEOUT = 5
|
|
||||||
-- facility authentication key (do NOT use one of your passwords)
|
|
||||||
-- this enables verifying that messages are authentic
|
|
||||||
-- all devices on the same network must use the same key
|
|
||||||
-- config.AUTH_KEY = "SCADAfacility123"
|
|
||||||
|
|
||||||
-- log path
|
|
||||||
config.LOG_PATH = "/log.txt"
|
|
||||||
-- log mode
|
|
||||||
-- 0 = APPEND (adds to existing file on start)
|
|
||||||
-- 1 = NEW (replaces existing file on start)
|
|
||||||
config.LOG_MODE = 0
|
|
||||||
-- true to log verbose debug messages
|
|
||||||
config.LOG_DEBUG = false
|
|
||||||
|
|
||||||
return config
|
|
||||||
578
pocket/configure.lua
Normal file
578
pocket/configure.lua
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
--
|
||||||
|
-- Configuration GUI
|
||||||
|
--
|
||||||
|
|
||||||
|
local log = require("scada-common.log")
|
||||||
|
local tcd = require("scada-common.tcd")
|
||||||
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local DisplayBox = require("graphics.elements.displaybox")
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local ListBox = require("graphics.elements.listbox")
|
||||||
|
local MultiPane = require("graphics.elements.multipane")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
local CheckBox = require("graphics.elements.controls.checkbox")
|
||||||
|
local PushButton = require("graphics.elements.controls.push_button")
|
||||||
|
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||||
|
|
||||||
|
local NumberField = require("graphics.elements.form.number_field")
|
||||||
|
local TextField = require("graphics.elements.form.text_field")
|
||||||
|
|
||||||
|
local println = util.println
|
||||||
|
local tri = util.trinary
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
local LEFT = core.ALIGN.LEFT
|
||||||
|
local CENTER = core.ALIGN.CENTER
|
||||||
|
local RIGHT = core.ALIGN.RIGHT
|
||||||
|
|
||||||
|
-- changes to the config data/format to let the user know
|
||||||
|
local changes = {}
|
||||||
|
|
||||||
|
---@class pkt_configurator
|
||||||
|
local configurator = {}
|
||||||
|
|
||||||
|
local style = {}
|
||||||
|
|
||||||
|
style.root = cpair(colors.black, colors.lightGray)
|
||||||
|
style.header = cpair(colors.white, colors.gray)
|
||||||
|
|
||||||
|
style.colors = {
|
||||||
|
{ c = colors.red, hex = 0xdf4949 },
|
||||||
|
{ c = colors.orange, hex = 0xffb659 },
|
||||||
|
{ c = colors.yellow, hex = 0xfffc79 },
|
||||||
|
{ c = colors.lime, hex = 0x80ff80 },
|
||||||
|
{ c = colors.green, hex = 0x4aee8a },
|
||||||
|
{ c = colors.cyan, hex = 0x34bac8 },
|
||||||
|
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||||
|
{ c = colors.blue, hex = 0x0096ff },
|
||||||
|
{ c = colors.purple, hex = 0xb156ee },
|
||||||
|
{ c = colors.pink, hex = 0xf26ba2 },
|
||||||
|
{ c = colors.magenta, hex = 0xf9488a },
|
||||||
|
{ c = colors.lightGray, hex = 0xcacaca },
|
||||||
|
{ c = colors.gray, hex = 0x575757 }
|
||||||
|
}
|
||||||
|
|
||||||
|
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||||
|
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||||
|
local nav_fg_bg = bw_fg_bg
|
||||||
|
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||||
|
local dis_fg_bg = cpair(colors.lightGray,colors.white)
|
||||||
|
|
||||||
|
local tool_ctl = {
|
||||||
|
ask_config = false,
|
||||||
|
has_config = false,
|
||||||
|
viewing_config = false,
|
||||||
|
importing_legacy = false,
|
||||||
|
|
||||||
|
view_cfg = nil, ---@type graphics_element
|
||||||
|
settings_apply = nil, ---@type graphics_element
|
||||||
|
|
||||||
|
set_networked = nil, ---@type function
|
||||||
|
bundled_emcool = nil, ---@type function
|
||||||
|
gen_summary = nil, ---@type function
|
||||||
|
show_current_cfg = nil, ---@type function
|
||||||
|
load_legacy = nil, ---@type function
|
||||||
|
|
||||||
|
show_auth_key = nil, ---@type function
|
||||||
|
show_key_btn = nil, ---@type graphics_element
|
||||||
|
auth_key_textbox = nil, ---@type graphics_element
|
||||||
|
auth_key_value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class pkt_config
|
||||||
|
local tmp_cfg = {
|
||||||
|
SVR_Channel = nil, ---@type integer
|
||||||
|
CRD_Channel = nil, ---@type integer
|
||||||
|
PKT_Channel = nil, ---@type integer
|
||||||
|
ConnTimeout = nil, ---@type number
|
||||||
|
TrustedRange = nil, ---@type number
|
||||||
|
AuthKey = nil, ---@type string|nil
|
||||||
|
LogMode = 0,
|
||||||
|
LogPath = "",
|
||||||
|
LogDebug = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class pkt_config
|
||||||
|
local ini_cfg = {}
|
||||||
|
---@class pkt_config
|
||||||
|
local settings_cfg = {}
|
||||||
|
|
||||||
|
-- all settings fields, their nice names, and their default values
|
||||||
|
local fields = {
|
||||||
|
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||||
|
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||||
|
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||||
|
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||||
|
{ "TrustedRange", "Trusted Range", 0 },
|
||||||
|
{ "AuthKey", "Facility Auth Key" , ""},
|
||||||
|
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||||
|
{ "LogPath", "Log Path", "/log.txt" },
|
||||||
|
{ "LogDebug","Log Debug Messages", false }
|
||||||
|
}
|
||||||
|
|
||||||
|
-- load data from the settings file
|
||||||
|
---@param target pkt_config
|
||||||
|
---@param raw boolean? true to not use default values
|
||||||
|
local function load_settings(target, raw)
|
||||||
|
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||||
|
|
||||||
|
local loaded = settings.load("/pocket.settings")
|
||||||
|
|
||||||
|
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||||
|
|
||||||
|
return loaded
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create the config view
|
||||||
|
---@param display graphics_element
|
||||||
|
local function config_view(display)
|
||||||
|
---@diagnostic disable-next-line: undefined-field
|
||||||
|
local function exit() os.queueEvent("terminate") end
|
||||||
|
|
||||||
|
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,fg_bg=style.header}
|
||||||
|
|
||||||
|
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||||
|
|
||||||
|
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
|
||||||
|
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}}
|
||||||
|
|
||||||
|
-- Main Page
|
||||||
|
|
||||||
|
local y_start = 7
|
||||||
|
|
||||||
|
TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."}
|
||||||
|
|
||||||
|
if tool_ctl.ask_config then
|
||||||
|
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||||
|
y_start = y_start + 3
|
||||||
|
end
|
||||||
|
|
||||||
|
local function view_config()
|
||||||
|
tool_ctl.viewing_config = true
|
||||||
|
tool_ctl.gen_summary(settings_cfg)
|
||||||
|
tool_ctl.settings_apply.hide(true)
|
||||||
|
main_pane.set_value(4)
|
||||||
|
end
|
||||||
|
|
||||||
|
if fs.exists("/pocket/config.lua") then
|
||||||
|
PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
|
||||||
|
y_start = y_start + 2
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||||
|
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||||
|
|
||||||
|
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
||||||
|
|
||||||
|
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
--#region Network
|
||||||
|
|
||||||
|
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||||
|
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||||
|
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||||
|
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||||
|
|
||||||
|
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||||
|
|
||||||
|
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_1,x=1,y=1,height=1,text="Set network channels."}
|
||||||
|
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"}
|
||||||
|
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||||
|
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"}
|
||||||
|
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||||
|
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"}
|
||||||
|
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||||
|
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
local chan_err = TextBox{parent=net_c_1,x=1,y=14,height=1,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
|
local function submit_channels()
|
||||||
|
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||||
|
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
|
||||||
|
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
|
||||||
|
net_pane.set_value(2)
|
||||||
|
chan_err.hide(true)
|
||||||
|
else chan_err.show() end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Set connection timeout."}
|
||||||
|
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_2,x=1,y=11,height=1,width=19,text="Connection Timeout"}
|
||||||
|
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
local ct_err = TextBox{parent=net_c_2,x=1,y=14,height=1,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
|
local function submit_timeouts()
|
||||||
|
local timeout_val = tonumber(timeout.get_value())
|
||||||
|
if timeout_val ~= nil then
|
||||||
|
tmp_cfg.ConnTimeout = timeout_val
|
||||||
|
net_pane.set_value(3)
|
||||||
|
ct_err.hide(true)
|
||||||
|
else ct_err.show() end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_3,x=1,y=1,height=1,text="Set the trusted range."}
|
||||||
|
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
|
||||||
|
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
local tr_err = TextBox{parent=net_c_3,x=1,y=14,height=1,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
|
local function submit_tr()
|
||||||
|
local range_val = tonumber(range.get_value())
|
||||||
|
if range_val ~= nil then
|
||||||
|
tmp_cfg.TrustedRange = range_val
|
||||||
|
net_pane.set_value(4)
|
||||||
|
tr_err.hide(true)
|
||||||
|
else tr_err.show() end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
|
||||||
|
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"}
|
||||||
|
local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||||
|
|
||||||
|
-- declare back first so tabbing makes sense visually
|
||||||
|
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||||
|
|
||||||
|
hide_key.set_value(true)
|
||||||
|
censor_key(true)
|
||||||
|
|
||||||
|
local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
|
local function submit_auth()
|
||||||
|
local v = key.get_value()
|
||||||
|
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||||
|
tmp_cfg.AuthKey = key.get_value()
|
||||||
|
main_pane.set_value(3)
|
||||||
|
key_err.hide(true)
|
||||||
|
else key_err.show() end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
--#endregion
|
||||||
|
|
||||||
|
--#region Logging
|
||||||
|
|
||||||
|
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
|
||||||
|
|
||||||
|
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||||
|
|
||||||
|
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Configure logging below."}
|
||||||
|
|
||||||
|
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
|
||||||
|
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||||
|
|
||||||
|
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
|
||||||
|
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||||
|
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
|
local path_err = TextBox{parent=log_c_1,x=1,y=14,height=1,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
|
local function submit_log()
|
||||||
|
if path.get_value() ~= "" then
|
||||||
|
path_err.hide(true)
|
||||||
|
tmp_cfg.LogMode = mode.get_value() - 1
|
||||||
|
tmp_cfg.LogPath = path.get_value()
|
||||||
|
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||||
|
tool_ctl.gen_summary(tmp_cfg)
|
||||||
|
tool_ctl.viewing_config = false
|
||||||
|
tool_ctl.importing_legacy = false
|
||||||
|
tool_ctl.settings_apply.show()
|
||||||
|
main_pane.set_value(4)
|
||||||
|
else path_err.show() end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
--#endregion
|
||||||
|
|
||||||
|
--#region Summary and Saving
|
||||||
|
|
||||||
|
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
|
||||||
|
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
|
||||||
|
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
|
||||||
|
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
|
||||||
|
|
||||||
|
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||||
|
|
||||||
|
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||||
|
|
||||||
|
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||||
|
|
||||||
|
local function back_from_summary()
|
||||||
|
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||||
|
main_pane.set_value(1)
|
||||||
|
tool_ctl.viewing_config = false
|
||||||
|
tool_ctl.importing_legacy = false
|
||||||
|
tool_ctl.settings_apply.show()
|
||||||
|
else
|
||||||
|
main_pane.set_value(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param element graphics_element
|
||||||
|
---@param data any
|
||||||
|
local function try_set(element, data)
|
||||||
|
if data ~= nil then element.set_value(data) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save_and_continue()
|
||||||
|
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||||
|
|
||||||
|
if settings.save("/pocket.settings") then
|
||||||
|
load_settings(settings_cfg, true)
|
||||||
|
load_settings(ini_cfg)
|
||||||
|
|
||||||
|
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||||
|
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||||
|
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||||
|
try_set(timeout, ini_cfg.ConnTimeout)
|
||||||
|
try_set(range, ini_cfg.TrustedRange)
|
||||||
|
try_set(key, ini_cfg.AuthKey)
|
||||||
|
try_set(mode, ini_cfg.LogMode)
|
||||||
|
try_set(path, ini_cfg.LogPath)
|
||||||
|
try_set(en_dbg, ini_cfg.LogDebug)
|
||||||
|
|
||||||
|
tool_ctl.view_cfg.enable()
|
||||||
|
|
||||||
|
if tool_ctl.importing_legacy then
|
||||||
|
tool_ctl.importing_legacy = false
|
||||||
|
sum_pane.set_value(3)
|
||||||
|
else
|
||||||
|
sum_pane.set_value(2)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
sum_pane.set_value(4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||||
|
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
|
||||||
|
|
||||||
|
local function go_home()
|
||||||
|
main_pane.set_value(1)
|
||||||
|
net_pane.set_value(1)
|
||||||
|
sum_pane.set_value(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
|
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||||
|
|
||||||
|
local function delete_legacy()
|
||||||
|
fs.delete("/pocket/config.lua")
|
||||||
|
exit()
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
|
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||||
|
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
|
--#endregion
|
||||||
|
|
||||||
|
-- Config Change Log
|
||||||
|
|
||||||
|
local cl = Div{parent=changelog,x=2,y=4,width=24}
|
||||||
|
|
||||||
|
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||||
|
|
||||||
|
for _, change in ipairs(changes) do
|
||||||
|
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
|
||||||
|
for _, v in ipairs(change[2]) do
|
||||||
|
local e = Div{parent=c_log,height=#util.strwrap(v,21)}
|
||||||
|
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
|
||||||
|
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
|
-- set tool functions now that we have the elements
|
||||||
|
|
||||||
|
-- load a legacy config file
|
||||||
|
function tool_ctl.load_legacy()
|
||||||
|
local config = require("pocket.config")
|
||||||
|
|
||||||
|
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||||
|
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
|
||||||
|
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
|
||||||
|
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
|
||||||
|
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||||
|
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||||
|
|
||||||
|
tmp_cfg.LogMode = config.LOG_MODE
|
||||||
|
tmp_cfg.LogPath = config.LOG_PATH
|
||||||
|
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||||
|
|
||||||
|
tool_ctl.gen_summary(tmp_cfg)
|
||||||
|
sum_pane.set_value(1)
|
||||||
|
main_pane.set_value(4)
|
||||||
|
tool_ctl.importing_legacy = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- expose the auth key on the summary page
|
||||||
|
function tool_ctl.show_auth_key()
|
||||||
|
tool_ctl.show_key_btn.disable()
|
||||||
|
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- generate the summary list
|
||||||
|
---@param cfg pkt_config
|
||||||
|
function tool_ctl.gen_summary(cfg)
|
||||||
|
setting_list.remove_all()
|
||||||
|
|
||||||
|
local alternate = false
|
||||||
|
local inner_width = setting_list.get_width() - 1
|
||||||
|
|
||||||
|
tool_ctl.show_key_btn.enable()
|
||||||
|
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||||
|
|
||||||
|
for i = 1, #fields do
|
||||||
|
local f = fields[i]
|
||||||
|
local height = 1
|
||||||
|
local label_w = string.len(f[2])
|
||||||
|
local val_max_w = (inner_width - label_w) - 1
|
||||||
|
local raw = cfg[f[1]]
|
||||||
|
local val = util.strval(raw)
|
||||||
|
|
||||||
|
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||||
|
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
||||||
|
|
||||||
|
if val == "nil" then val = "<not set>" end
|
||||||
|
|
||||||
|
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||||
|
alternate = not alternate
|
||||||
|
|
||||||
|
if string.len(val) > val_max_w then
|
||||||
|
local lines = util.strwrap(val, inner_width)
|
||||||
|
height = #lines + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local line = Div{parent=setting_list,height=height,fg_bg=c}
|
||||||
|
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
|
||||||
|
|
||||||
|
local textbox
|
||||||
|
if height > 1 then
|
||||||
|
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||||
|
else
|
||||||
|
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||||
|
end
|
||||||
|
|
||||||
|
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- reset terminal screen
|
||||||
|
local function reset_term()
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- run the pcoket configurator
|
||||||
|
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
|
||||||
|
function configurator.configure(ask_config)
|
||||||
|
tool_ctl.ask_config = ask_config == true
|
||||||
|
|
||||||
|
load_settings(settings_cfg, true)
|
||||||
|
tool_ctl.has_config = load_settings(ini_cfg)
|
||||||
|
|
||||||
|
reset_term()
|
||||||
|
|
||||||
|
-- set overridden colors
|
||||||
|
for i = 1, #style.colors do
|
||||||
|
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||||
|
end
|
||||||
|
|
||||||
|
local status, error = pcall(function ()
|
||||||
|
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||||
|
config_view(display)
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local event, param1, param2, param3 = util.pull_event()
|
||||||
|
|
||||||
|
-- handle event
|
||||||
|
if event == "timer" then
|
||||||
|
tcd.handle(param1)
|
||||||
|
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||||
|
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||||
|
if m_e then display.handle_mouse(m_e) end
|
||||||
|
elseif event == "char" or event == "key" or event == "key_up" then
|
||||||
|
local k_e = core.events.new_key_event(event, param1, param2)
|
||||||
|
if k_e then display.handle_key(k_e) end
|
||||||
|
elseif event == "paste" then
|
||||||
|
display.handle_paste(param1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if event == "terminate" then return end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- restore colors
|
||||||
|
for i = 1, #style.colors do
|
||||||
|
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||||
|
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||||
|
end
|
||||||
|
|
||||||
|
reset_term()
|
||||||
|
if not status then
|
||||||
|
println("configurator error: " .. error)
|
||||||
|
end
|
||||||
|
|
||||||
|
return status, error
|
||||||
|
end
|
||||||
|
|
||||||
|
return configurator
|
||||||
@@ -13,17 +13,57 @@ local LINK_STATE = iocontrol.LINK_STATE
|
|||||||
|
|
||||||
local pocket = {}
|
local pocket = {}
|
||||||
|
|
||||||
|
---@type pkt_config
|
||||||
|
local config = {}
|
||||||
|
|
||||||
|
pocket.config = config
|
||||||
|
|
||||||
|
-- load the pocket configuration
|
||||||
|
function pocket.load_config()
|
||||||
|
if not settings.load("/pocket.settings") then return false end
|
||||||
|
|
||||||
|
config.SVR_Channel = settings.get("SVR_Channel")
|
||||||
|
config.CRD_Channel = settings.get("CRD_Channel")
|
||||||
|
config.PKT_Channel = settings.get("PKT_Channel")
|
||||||
|
config.ConnTimeout = settings.get("ConnTimeout")
|
||||||
|
config.TrustedRange = settings.get("TrustedRange")
|
||||||
|
config.AuthKey = settings.get("AuthKey")
|
||||||
|
|
||||||
|
config.LogMode = settings.get("LogMode")
|
||||||
|
config.LogPath = settings.get("LogPath")
|
||||||
|
config.LogDebug = settings.get("LogDebug")
|
||||||
|
|
||||||
|
local cfv = util.new_validator()
|
||||||
|
|
||||||
|
cfv.assert_channel(config.SVR_Channel)
|
||||||
|
cfv.assert_channel(config.CRD_Channel)
|
||||||
|
cfv.assert_channel(config.PKT_Channel)
|
||||||
|
cfv.assert_type_num(config.ConnTimeout)
|
||||||
|
cfv.assert_min(config.ConnTimeout, 2)
|
||||||
|
cfv.assert_type_num(config.TrustedRange)
|
||||||
|
cfv.assert_min(config.TrustedRange, 0)
|
||||||
|
cfv.assert_type_str(config.AuthKey)
|
||||||
|
|
||||||
|
if type(config.AuthKey) == "string" then
|
||||||
|
local len = string.len(config.AuthKey)
|
||||||
|
cfv.assert(len == 0 or len >= 8)
|
||||||
|
end
|
||||||
|
|
||||||
|
cfv.assert_type_int(config.LogMode)
|
||||||
|
cfv.assert_range(config.LogMode, 0, 1)
|
||||||
|
cfv.assert_type_str(config.LogPath)
|
||||||
|
cfv.assert_type_bool(config.LogDebug)
|
||||||
|
|
||||||
|
return cfv.valid()
|
||||||
|
end
|
||||||
|
|
||||||
-- pocket coordinator + supervisor communications
|
-- pocket coordinator + supervisor communications
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param version string pocket version
|
---@param version string pocket version
|
||||||
---@param nic nic network interface device
|
---@param nic nic network interface device
|
||||||
---@param pkt_channel integer pocket comms channel
|
|
||||||
---@param svr_channel integer supervisor access channel
|
|
||||||
---@param crd_channel integer coordinator access channel
|
|
||||||
---@param range integer trusted device connection range
|
|
||||||
---@param sv_watchdog watchdog
|
---@param sv_watchdog watchdog
|
||||||
---@param api_watchdog watchdog
|
---@param api_watchdog watchdog
|
||||||
function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog)
|
function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||||
local self = {
|
local self = {
|
||||||
sv = {
|
sv = {
|
||||||
linked = false,
|
linked = false,
|
||||||
@@ -42,13 +82,13 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
establish_delay_counter = 0
|
establish_delay_counter = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
comms.set_trusted_range(range)
|
comms.set_trusted_range(config.TrustedRange)
|
||||||
|
|
||||||
-- PRIVATE FUNCTIONS --
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
-- configure network channels
|
-- configure network channels
|
||||||
nic.closeAll()
|
nic.closeAll()
|
||||||
nic.open(pkt_channel)
|
nic.open(config.PKT_Channel)
|
||||||
|
|
||||||
-- send a management packet to the supervisor
|
-- send a management packet to the supervisor
|
||||||
---@param msg_type MGMT_TYPE
|
---@param msg_type MGMT_TYPE
|
||||||
@@ -60,7 +100,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
pkt.make(msg_type, msg)
|
pkt.make(msg_type, msg)
|
||||||
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||||
|
|
||||||
nic.transmit(svr_channel, pkt_channel, s_pkt)
|
nic.transmit(config.SVR_Channel, config.PKT_Channel, s_pkt)
|
||||||
self.sv.seq_num = self.sv.seq_num + 1
|
self.sv.seq_num = self.sv.seq_num + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,7 +114,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
pkt.make(msg_type, msg)
|
pkt.make(msg_type, msg)
|
||||||
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
|
||||||
|
|
||||||
nic.transmit(crd_channel, pkt_channel, s_pkt)
|
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
|
||||||
self.api.seq_num = self.api.seq_num + 1
|
self.api.seq_num = self.api.seq_num + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -217,9 +257,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
local protocol = packet.scada_frame.protocol()
|
local protocol = packet.scada_frame.protocol()
|
||||||
local src_addr = packet.scada_frame.src_addr()
|
local src_addr = packet.scada_frame.src_addr()
|
||||||
|
|
||||||
if l_chan ~= pkt_channel then
|
if l_chan ~= config.PKT_Channel then
|
||||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||||
elseif r_chan == crd_channel then
|
elseif r_chan == config.CRD_Channel then
|
||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.api.r_seq_num == nil then
|
if self.api.r_seq_num == nil then
|
||||||
self.api.r_seq_num = packet.scada_frame.seq_num()
|
self.api.r_seq_num = packet.scada_frame.seq_num()
|
||||||
@@ -308,7 +348,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
else
|
else
|
||||||
log.debug("illegal packet type " .. protocol .. " from coordinator", true)
|
log.debug("illegal packet type " .. protocol .. " from coordinator", true)
|
||||||
end
|
end
|
||||||
elseif r_chan == svr_channel then
|
elseif r_chan == config.SVR_Channel then
|
||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.sv.r_seq_num == nil then
|
if self.sv.r_seq_num == nil then
|
||||||
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
self.sv.r_seq_num = packet.scada_frame.seq_num()
|
||||||
|
|||||||
@@ -13,38 +13,37 @@ local util = require("scada-common.util")
|
|||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local config = require("pocket.config")
|
local configure = require("pocket.configure")
|
||||||
local iocontrol = require("pocket.iocontrol")
|
local iocontrol = require("pocket.iocontrol")
|
||||||
local pocket = require("pocket.pocket")
|
local pocket = require("pocket.pocket")
|
||||||
local renderer = require("pocket.renderer")
|
local renderer = require("pocket.renderer")
|
||||||
|
|
||||||
local POCKET_VERSION = "v0.6.3-alpha"
|
local POCKET_VERSION = "v0.7.0-alpha"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- config validation
|
-- get configuration
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
local cfv = util.new_validator()
|
if not pocket.load_config() then
|
||||||
|
-- try to reconfigure (user action)
|
||||||
|
local success, error = configure.configure(true)
|
||||||
|
if success then
|
||||||
|
assert(pocket.load_config(), "failed to load valid configuration")
|
||||||
|
else
|
||||||
|
assert(success, "pocket configuration error: " .. error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
cfv.assert_channel(config.SVR_CHANNEL)
|
local config = pocket.config
|
||||||
cfv.assert_channel(config.CRD_CHANNEL)
|
|
||||||
cfv.assert_channel(config.PKT_CHANNEL)
|
|
||||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
|
||||||
cfv.assert_type_num(config.COMMS_TIMEOUT)
|
|
||||||
cfv.assert_min(config.COMMS_TIMEOUT, 2)
|
|
||||||
cfv.assert_type_str(config.LOG_PATH)
|
|
||||||
cfv.assert_type_int(config.LOG_MODE)
|
|
||||||
|
|
||||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING pocket.startup " .. POCKET_VERSION)
|
log.info("BOOTING pocket.startup " .. POCKET_VERSION)
|
||||||
@@ -69,8 +68,8 @@ local function main()
|
|||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
-- message authentication init
|
-- message authentication init
|
||||||
if type(config.AUTH_KEY) == "string" then
|
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||||
network.init_mac(config.AUTH_KEY)
|
network.init_mac(config.AuthKey)
|
||||||
end
|
end
|
||||||
|
|
||||||
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
||||||
@@ -85,8 +84,8 @@ local function main()
|
|||||||
|
|
||||||
-- create connection watchdogs
|
-- create connection watchdogs
|
||||||
local conn_wd = {
|
local conn_wd = {
|
||||||
sv = util.new_watchdog(config.COMMS_TIMEOUT),
|
sv = util.new_watchdog(config.ConnTimeout),
|
||||||
api = util.new_watchdog(config.COMMS_TIMEOUT)
|
api = util.new_watchdog(config.ConnTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn_wd.sv.cancel()
|
conn_wd.sv.cancel()
|
||||||
@@ -96,8 +95,7 @@ local function main()
|
|||||||
|
|
||||||
-- create network interface then setup comms
|
-- create network interface then setup comms
|
||||||
local nic = network.nic(modem)
|
local nic = network.nic(modem)
|
||||||
local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL,
|
local pocket_comms = pocket.comms(POCKET_VERSION, nic, conn_wd.sv, conn_wd.api)
|
||||||
config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api)
|
|
||||||
log.debug("startup> comms init")
|
log.debug("startup> comms init")
|
||||||
|
|
||||||
-- base loop clock (2Hz, 10 ticks)
|
-- base loop clock (2Hz, 10 ticks)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
--
|
--
|
||||||
|
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
|
local rsio = require("scada-common.rsio")
|
||||||
local tcd = require("scada-common.tcd")
|
local tcd = require("scada-common.tcd")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ local NumberField = require("graphics.elements.form.number_field")
|
|||||||
local TextField = require("graphics.elements.form.text_field")
|
local TextField = require("graphics.elements.form.text_field")
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
|
local tri = util.trinary
|
||||||
|
|
||||||
local cpair = core.cpair
|
local cpair = core.cpair
|
||||||
|
|
||||||
@@ -30,6 +32,12 @@ local LEFT = core.ALIGN.LEFT
|
|||||||
local CENTER = core.ALIGN.CENTER
|
local CENTER = core.ALIGN.CENTER
|
||||||
local RIGHT = core.ALIGN.RIGHT
|
local RIGHT = core.ALIGN.RIGHT
|
||||||
|
|
||||||
|
-- changes to the config data/format to let the user know
|
||||||
|
local changes = {
|
||||||
|
{"v1.6.2", { "AuthKey minimum length is now 8 (if set)" } },
|
||||||
|
{"v1.6.8", { "ConnTimeout can now have a fractional part" } }
|
||||||
|
}
|
||||||
|
|
||||||
---@class plc_configurator
|
---@class plc_configurator
|
||||||
local configurator = {}
|
local configurator = {}
|
||||||
|
|
||||||
@@ -37,7 +45,6 @@ local style = {}
|
|||||||
|
|
||||||
style.root = cpair(colors.black, colors.lightGray)
|
style.root = cpair(colors.black, colors.lightGray)
|
||||||
style.header = cpair(colors.white, colors.gray)
|
style.header = cpair(colors.white, colors.gray)
|
||||||
style.label = cpair(colors.gray, colors.lightGray)
|
|
||||||
|
|
||||||
style.colors = {
|
style.colors = {
|
||||||
{ c = colors.red, hex = 0xdf4949 },
|
{ c = colors.red, hex = 0xdf4949 },
|
||||||
@@ -55,6 +62,11 @@ style.colors = {
|
|||||||
{ c = colors.gray, hex = 0x575757 }
|
{ c = colors.gray, hex = 0x575757 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||||
|
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||||
|
local nav_fg_bg = bw_fg_bg
|
||||||
|
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||||
|
|
||||||
local tool_ctl = {
|
local tool_ctl = {
|
||||||
ask_config = false,
|
ask_config = false,
|
||||||
has_config = false,
|
has_config = false,
|
||||||
@@ -81,13 +93,13 @@ local tmp_cfg = {
|
|||||||
Networked = false,
|
Networked = false,
|
||||||
UnitID = 0,
|
UnitID = 0,
|
||||||
EmerCoolEnable = false,
|
EmerCoolEnable = false,
|
||||||
EmerCoolSide = nil,
|
EmerCoolSide = nil, ---@type string|nil
|
||||||
EmerCoolColor = nil,
|
EmerCoolColor = nil, ---@type color|nil
|
||||||
SVR_Channel = nil,
|
SVR_Channel = nil, ---@type integer
|
||||||
PLC_Channel = nil,
|
PLC_Channel = nil, ---@type integer
|
||||||
ConnTimeout = nil,
|
ConnTimeout = nil, ---@type number
|
||||||
TrustedRange = nil,
|
TrustedRange = nil, ---@type number
|
||||||
AuthKey = nil,
|
AuthKey = nil, ---@type string|nil
|
||||||
LogMode = 0,
|
LogMode = 0,
|
||||||
LogPath = "",
|
LogPath = "",
|
||||||
LogDebug = false,
|
LogDebug = false,
|
||||||
@@ -95,21 +107,24 @@ local tmp_cfg = {
|
|||||||
|
|
||||||
---@class plc_config
|
---@class plc_config
|
||||||
local ini_cfg = {}
|
local ini_cfg = {}
|
||||||
|
---@class plc_config
|
||||||
|
local settings_cfg = {}
|
||||||
|
|
||||||
|
-- all settings fields, their nice names, and their default values
|
||||||
local fields = {
|
local fields = {
|
||||||
{ "Networked", "Networked" },
|
{ "Networked", "Networked", false },
|
||||||
{ "UnitID", "Unit ID" },
|
{ "UnitID", "Unit ID", 1 },
|
||||||
{ "EmerCoolEnable", "Emergency Coolant" },
|
{ "EmerCoolEnable", "Emergency Coolant", false },
|
||||||
{ "EmerCoolSide", "Emergency Coolant Side" },
|
{ "EmerCoolSide", "Emergency Coolant Side", nil },
|
||||||
{ "EmerCoolColor", "Emergency Coolant Color" },
|
{ "EmerCoolColor", "Emergency Coolant Color", nil },
|
||||||
{ "SVR_Channel", "SVR Channel" },
|
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||||
{ "PLC_Channel", "PLC Channel" },
|
{ "PLC_Channel", "PLC Channel", 16241 },
|
||||||
{ "ConnTimeout", "Connection Timeout" },
|
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||||
{ "TrustedRange", "Trusted Range" },
|
{ "TrustedRange", "Trusted Range", 0 },
|
||||||
{ "AuthKey", "Facility Auth Key" },
|
{ "AuthKey", "Facility Auth Key" , ""},
|
||||||
{ "LogMode", "Log Mode" },
|
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||||
{ "LogPath", "Log Path" },
|
{ "LogPath", "Log Path", "/log.txt" },
|
||||||
{ "LogDebug","Log Debug Messages" }
|
{ "LogDebug","Log Debug Messages", false }
|
||||||
}
|
}
|
||||||
|
|
||||||
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
|
local side_options = { "Top", "Bottom", "Left", "Right", "Front", "Back" }
|
||||||
@@ -117,25 +132,6 @@ local side_options_map = { "top", "bottom", "left", "right", "front", "back" }
|
|||||||
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
|
local color_options = { "Red", "Orange", "Yellow", "Lime", "Green", "Cyan", "Light Blue", "Blue", "Purple", "Magenta", "Pink", "White", "Light Gray", "Gray", "Black", "Brown" }
|
||||||
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
|
local color_options_map = { colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.white, colors.lightGray, colors.gray, colors.black, colors.brown }
|
||||||
|
|
||||||
local color_name_map = {
|
|
||||||
[colors.red] = "red",
|
|
||||||
[colors.orange] = "orange",
|
|
||||||
[colors.yellow] = "yellow",
|
|
||||||
[colors.lime] = "lime",
|
|
||||||
[colors.green] = "green",
|
|
||||||
[colors.cyan] = "cyan",
|
|
||||||
[colors.lightBlue] = "lightBlue",
|
|
||||||
[colors.blue] = "blue",
|
|
||||||
[colors.purple] = "purple",
|
|
||||||
[colors.magenta] = "magenta",
|
|
||||||
[colors.pink] = "pink",
|
|
||||||
[colors.white] = "white",
|
|
||||||
[colors.lightGray] = "lightGray",
|
|
||||||
[colors.gray] = "gray",
|
|
||||||
[colors.black] = "black",
|
|
||||||
[colors.brown] = "brown"
|
|
||||||
}
|
|
||||||
|
|
||||||
-- convert text representation to index
|
-- convert text representation to index
|
||||||
---@param side string
|
---@param side string
|
||||||
local function side_to_idx(side)
|
local function side_to_idx(side)
|
||||||
@@ -154,27 +150,20 @@ end
|
|||||||
|
|
||||||
-- load data from the settings file
|
-- load data from the settings file
|
||||||
---@param target plc_config
|
---@param target plc_config
|
||||||
local function load_settings(target)
|
---@param raw boolean? true to not use default values
|
||||||
target.Networked = settings.get("Networked", false)
|
local function load_settings(target, raw)
|
||||||
target.UnitID = settings.get("UnitID", 1)
|
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||||
target.EmerCoolEnable = settings.get("EmerCoolEnable", false)
|
|
||||||
target.EmerCoolSide = settings.get("EmerCoolSide", nil)
|
local loaded = settings.load("/reactor-plc.settings")
|
||||||
target.EmerCoolColor = settings.get("EmerCoolColor", nil)
|
|
||||||
target.SVR_Channel = settings.get("SVR_Channel", 16240)
|
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||||
target.PLC_Channel = settings.get("PLC_Channel", 16241)
|
|
||||||
target.ConnTimeout = settings.get("ConnTimeout", 5)
|
return loaded
|
||||||
target.TrustedRange = settings.get("TrustedRange", 0)
|
|
||||||
target.AuthKey = settings.get("AuthKey", "")
|
|
||||||
target.LogMode = settings.get("LogMode", log.MODE.APPEND)
|
|
||||||
target.LogPath = settings.get("LogPath", "/log.txt")
|
|
||||||
target.LogDebug = settings.get("LogDebug", false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create the config view
|
-- create the config view
|
||||||
---@param display graphics_element
|
---@param display graphics_element
|
||||||
local function config_view(display)
|
local function config_view(display)
|
||||||
local nav_fg_bg = cpair(colors.black,colors.white)
|
|
||||||
local btn_act_fg_bg = cpair(colors.white,colors.gray)
|
|
||||||
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
local function exit() os.queueEvent("terminate") end
|
local function exit() os.queueEvent("terminate") end
|
||||||
@@ -188,23 +177,24 @@ local function config_view(display)
|
|||||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||||
|
|
||||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary}}
|
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,plc_cfg,net_cfg,log_cfg,summary,changelog}}
|
||||||
|
|
||||||
-- MAIN PAGE
|
-- Main Page
|
||||||
|
|
||||||
local y_start = 5
|
local y_start = 5
|
||||||
|
|
||||||
TextBox{parent=main_page,x=2,y=2,height=2,text_align=CENTER,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
|
TextBox{parent=main_page,x=2,y=2,height=2,text="Welcome to the Reactor PLC configurator! Please select one of the following options."}
|
||||||
|
|
||||||
if tool_ctl.ask_config then
|
if tool_ctl.ask_config then
|
||||||
TextBox{parent=main_page,x=2,y=y_start,height=2,text_align=CENTER,text="Notice: This device has no valid config. The configurator has been automatically started.",fg_bg=cpair(colors.red,colors.lightGray)}
|
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Notice: This device has no valid config so the configurator has been automatically started. If you previously had a valid config, you may want to check the Change Log to see what changed.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||||
y_start = y_start + 3
|
y_start = y_start + 5
|
||||||
end
|
end
|
||||||
|
|
||||||
local function view_config()
|
local function view_config()
|
||||||
tool_ctl.viewing_config = true
|
tool_ctl.viewing_config = true
|
||||||
tool_ctl.gen_summary(ini_cfg)
|
tool_ctl.gen_summary(settings_cfg)
|
||||||
tool_ctl.settings_apply.hide(true)
|
tool_ctl.settings_apply.hide(true)
|
||||||
main_pane.set_value(5)
|
main_pane.set_value(5)
|
||||||
end
|
end
|
||||||
@@ -220,8 +210,9 @@ local function config_view(display)
|
|||||||
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
||||||
|
|
||||||
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||||
|
PushButton{parent=main_page,x=39,y=17,min_width=12,text="Change Log",callback=function()main_pane.set_value(6)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
-- PLC CONFIG
|
--#region PLC
|
||||||
|
|
||||||
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
local plc_c_1 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||||
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
|
||||||
@@ -230,10 +221,10 @@ local function config_view(display)
|
|||||||
|
|
||||||
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
|
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}}
|
||||||
|
|
||||||
TextBox{parent=plc_cfg,x=1,y=2,height=1,text_align=CENTER,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
|
TextBox{parent=plc_cfg,x=1,y=2,height=1,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
|
||||||
|
|
||||||
TextBox{parent=plc_c_1,x=1,y=1,height=1,text_align=CENTER,text="Would you like to set this PLC as networked?"}
|
TextBox{parent=plc_c_1,x=1,y=1,height=1,text="Would you like to set this PLC as networked?"}
|
||||||
TextBox{parent=plc_c_1,x=1,y=3,height=4,text_align=CENTER,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=plc_c_1,x=1,y=3,height=4,text="If you have a supervisor, select the box. You will later be prompted to select the network configuration. If you instead want to use this as a standalone safety system, don't select the box.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
|
local networked = CheckBox{parent=plc_c_1,x=1,y=8,label="Networked",default=ini_cfg.Networked,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||||
|
|
||||||
@@ -242,16 +233,16 @@ local function config_view(display)
|
|||||||
plc_pane.set_value(2)
|
plc_pane.set_value(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=plc_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=plc_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_1,x=44,y=14,text="Next \x1a",callback=submit_networked,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=plc_c_2,x=1,y=1,height=1,text_align=CENTER,text="Please enter the reactor unit ID for this PLC."}
|
TextBox{parent=plc_c_2,x=1,y=1,height=1,text="Please enter the reactor unit ID for this PLC."}
|
||||||
TextBox{parent=plc_c_2,x=1,y=3,height=3,text_align=CENTER,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=plc_c_2,x=1,y=3,height=3,text="If this is a networked PLC, currently only IDs 1 through 4 are acceptable.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=plc_c_2,x=1,y=6,height=1,text_align=CENTER,text="Unit #"}
|
TextBox{parent=plc_c_2,x=1,y=6,height=1,text="Unit #"}
|
||||||
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_digits=3,default=ini_cfg.UnitID,min=1,fg_bg=cpair(colors.black,colors.white)}
|
local u_id = NumberField{parent=plc_c_2,x=7,y=6,width=5,max_chars=3,default=ini_cfg.UnitID,min=1,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
local u_id_err = TextBox{parent=plc_c_2,x=8,y=14,height=1,width=35,text="Please set a unit ID.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
local function submit_id()
|
local function submit_id()
|
||||||
local unit_id = tonumber(u_id.get_value())
|
local unit_id = tonumber(u_id.get_value())
|
||||||
@@ -262,11 +253,11 @@ local function config_view(display)
|
|||||||
else u_id_err.show() end
|
else u_id_err.show() end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=plc_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_2,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=plc_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_2,x=44,y=14,text="Next \x1a",callback=submit_id,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=plc_c_3,x=1,y=1,height=4,text_align=CENTER,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC. "}
|
TextBox{parent=plc_c_3,x=1,y=1,height=4,text="When networked, the supervisor takes care of emergency coolant via RTUs. However, you can configure independent emergency coolant via the PLC."}
|
||||||
TextBox{parent=plc_c_3,x=1,y=6,height=5,text_align=CENTER,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=plc_c_3,x=1,y=6,height=5,text="This independent control can be used with or without a supervisor. To configure, you would next select the interface of the redstone output connected to one or more mekanism pipes.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
|
local en_em_cool = CheckBox{parent=plc_c_3,x=1,y=11,label="Enable PLC Emergency Coolant Control",default=ini_cfg.EmerCoolEnable,box_fg_bg=cpair(colors.orange,colors.black)}
|
||||||
|
|
||||||
@@ -279,27 +270,29 @@ local function config_view(display)
|
|||||||
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
|
if tmp_cfg.EmerCoolEnable then plc_pane.set_value(4) else next_from_plc() end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=plc_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_3,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=plc_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_3,x=44,y=14,text="Next \x1a",callback=submit_en_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=plc_c_4,x=1,y=1,height=1,text_align=CENTER,text="Emergency Coolant Redstone Output Side"}
|
TextBox{parent=plc_c_4,x=1,y=1,height=1,text="Emergency Coolant Redstone Output Side"}
|
||||||
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
|
local side = Radio2D{parent=plc_c_4,x=1,y=2,rows=2,columns=3,default=side_to_idx(ini_cfg.EmerCoolSide),options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.orange}
|
||||||
|
|
||||||
TextBox{parent=plc_c_4,x=1,y=5,height=1,text_align=CENTER,text="Bundled Redstone Configuration"}
|
TextBox{parent=plc_c_4,x=1,y=5,height=1,text="Bundled Redstone Configuration"}
|
||||||
local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end}
|
local bundled = CheckBox{parent=plc_c_4,x=1,y=6,label="Is Bundled?",default=ini_cfg.EmerCoolColor~=nil,box_fg_bg=cpair(colors.orange,colors.black),callback=function(v)tool_ctl.bundled_emcool(v)end}
|
||||||
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=cpair(colors.gray,colors.lightGray)}
|
local color = Radio2D{parent=plc_c_4,x=1,y=8,rows=4,columns=4,default=color_to_idx(ini_cfg.EmerCoolColor),options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
|
||||||
if ini_cfg.EmerCoolColor == nil then color.disable() end
|
if ini_cfg.EmerCoolColor == nil then color.disable() end
|
||||||
|
|
||||||
local function submit_emcool()
|
local function submit_emcool()
|
||||||
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
|
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
|
||||||
tmp_cfg.EmerCoolColor = color_options_map[color.get_value()]
|
tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
|
||||||
next_from_plc()
|
next_from_plc()
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=plc_c_4,x=1,y=14,min_width=6,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_4,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=plc_c_4,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=plc_c_4,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
-- NET CONFIG
|
--#endregion
|
||||||
|
|
||||||
|
--#region Network
|
||||||
|
|
||||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
|
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=49}
|
||||||
@@ -307,19 +300,19 @@ local function config_view(display)
|
|||||||
|
|
||||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3}}
|
||||||
|
|
||||||
TextBox{parent=net_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||||
|
|
||||||
TextBox{parent=net_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please set the network channels below."}
|
TextBox{parent=net_c_1,x=1,y=1,height=1,text="Please set the network channels below."}
|
||||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text_align=CENTER,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the 5 uniquely named channels, including the 2 below, must be the same for each device in this SCADA network. For multiplayer servers, it is recommended to not use the default channels.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=net_c_1,x=1,y=8,height=1,text_align=CENTER,text="Supervisor Channel"}
|
TextBox{parent=net_c_1,x=1,y=8,height=1,text="Supervisor Channel"}
|
||||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)}
|
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text_align=CENTER,text="[SVR_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||||
TextBox{parent=net_c_1,x=1,y=11,height=1,text_align=CENTER,text="PLC Channel"}
|
TextBox{parent=net_c_1,x=1,y=11,height=1,text="PLC Channel"}
|
||||||
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=cpair(colors.black,colors.white)}
|
local plc_chan = NumberField{parent=net_c_1,x=1,y=12,width=7,default=ini_cfg.PLC_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||||
TextBox{parent=net_c_1,x=9,y=12,height=4,text_align=CENTER,text="[PLC_CHANNEL]",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_1,x=9,y=12,height=4,text="[PLC_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
local chan_err = TextBox{parent=net_c_1,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
local function submit_channels()
|
local function submit_channels()
|
||||||
local svr_c = tonumber(svr_chan.get_value())
|
local svr_c = tonumber(svr_chan.get_value())
|
||||||
@@ -338,19 +331,19 @@ local function config_view(display)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=net_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=net_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=net_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=net_c_1,x=44,y=14,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=net_c_2,x=1,y=1,height=1,text_align=CENTER,text="Connection Timeout"}
|
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Connection Timeout"}
|
||||||
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,fg_bg=cpair(colors.black,colors.white)}
|
local timeout = NumberField{parent=net_c_2,x=1,y=2,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||||
TextBox{parent=net_c_2,x=9,y=2,height=2,text_align=CENTER,text="seconds (default 5)",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_2,x=9,y=2,height=2,text="seconds (default 5)",fg_bg=g_lg_fg_bg}
|
||||||
TextBox{parent=net_c_2,x=1,y=3,height=4,text_align=CENTER,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_2,x=1,y=3,height=4,text="You generally do not want or need to modify this. On slow servers, you can increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=net_c_2,x=1,y=8,height=1,text_align=CENTER,text="Trusted Range"}
|
TextBox{parent=net_c_2,x=1,y=8,height=1,text="Trusted Range"}
|
||||||
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_digits=20,allow_decimal=true,fg_bg=cpair(colors.black,colors.white)}
|
local range = NumberField{parent=net_c_2,x=1,y=9,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||||
TextBox{parent=net_c_2,x=1,y=10,height=4,text_align=CENTER,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_2,x=1,y=10,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many meters (blocks) away in any direction.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text_align=LEFT,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
local p2_err = TextBox{parent=net_c_2,x=8,y=14,height=1,width=35,text="",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
local function submit_ct_tr()
|
local function submit_ct_tr()
|
||||||
local timeout_val = tonumber(timeout.get_value())
|
local timeout_val = tonumber(timeout.get_value())
|
||||||
@@ -369,14 +362,14 @@ local function config_view(display)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=net_c_2,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=net_c_2,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=net_c_2,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=net_c_2,x=44,y=14,text="Next \x1a",callback=submit_ct_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=net_c_3,x=1,y=1,height=2,text_align=CENTER,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
TextBox{parent=net_c_3,x=1,y=1,height=2,text="Optionally, set the facility authentication key below. Do NOT use one of your passwords."}
|
||||||
TextBox{parent=net_c_3,x=1,y=4,height=6,text_align=CENTER,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=net_c_3,x=1,y=4,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers. All devices on the same network MUST use the same key if any device has a key. This does result in some extra compution (can slow things down).",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=net_c_3,x=1,y=11,height=1,text_align=CENTER,text="Facility Auth Key"}
|
TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"}
|
||||||
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=cpair(colors.black,colors.white)}
|
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||||
|
|
||||||
@@ -385,32 +378,40 @@ local function config_view(display)
|
|||||||
hide_key.set_value(true)
|
hide_key.set_value(true)
|
||||||
censor_key(true)
|
censor_key(true)
|
||||||
|
|
||||||
|
local key_err = TextBox{parent=net_c_3,x=8,y=14,height=1,width=35,text="Key must be at least 8 characters.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
local function submit_auth()
|
local function submit_auth()
|
||||||
tmp_cfg.AuthKey = key.get_value()
|
local v = key.get_value()
|
||||||
main_pane.set_value(4)
|
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||||
|
tmp_cfg.AuthKey = key.get_value()
|
||||||
|
main_pane.set_value(4)
|
||||||
|
key_err.hide(true)
|
||||||
|
else key_err.show() end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=net_c_3,x=1,y=14,min_width=6,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=net_c_3,x=1,y=14,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=net_c_3,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=net_c_3,x=44,y=14,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
-- LOG CONFIG
|
--#endregion
|
||||||
|
|
||||||
|
--#region Logging
|
||||||
|
|
||||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=49}
|
||||||
|
|
||||||
TextBox{parent=log_cfg,x=1,y=2,height=1,text_align=CENTER,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||||
|
|
||||||
TextBox{parent=log_c_1,x=1,y=1,height=1,text_align=CENTER,text="Please configure logging below."}
|
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Please configure logging below."}
|
||||||
|
|
||||||
TextBox{parent=log_c_1,x=1,y=3,height=1,text_align=CENTER,text="Log File Mode"}
|
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
|
||||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||||
|
|
||||||
TextBox{parent=log_c_1,x=1,y=7,height=1,text_align=CENTER,text="Log File Path"}
|
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
|
||||||
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=cpair(colors.black,colors.white)}
|
local path = TextField{parent=log_c_1,x=1,y=8,width=49,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Logging Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||||
TextBox{parent=log_c_1,x=3,y=11,height=2,text_align=CENTER,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=cpair(colors.gray,colors.lightGray)}
|
TextBox{parent=log_c_1,x=3,y=11,height=2,text="This results in much larger log files. It is best to only use this when there is a problem.",fg_bg=g_lg_fg_bg}
|
||||||
|
|
||||||
local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text_align=LEFT,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
local path_err = TextBox{parent=log_c_1,x=8,y=14,height=1,width=35,text="Please provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||||
|
|
||||||
local function submit_log()
|
local function submit_log()
|
||||||
if path.get_value() ~= "" then
|
if path.get_value() ~= "" then
|
||||||
@@ -430,10 +431,12 @@ local function config_view(display)
|
|||||||
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
|
if tmp_cfg.Networked then main_pane.set_value(3) else main_pane.set_value(2) end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=log_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=log_c_1,x=1,y=14,text="\x1b Back",callback=back_from_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=log_c_1,x=44,y=14,min_width=6,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=log_c_1,x=44,y=14,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
-- SUMMARY OF CHANGES
|
--#endregion
|
||||||
|
|
||||||
|
--#region Summary and Saving
|
||||||
|
|
||||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
|
local sum_c_1 = Div{parent=summary,x=2,y=4,width=49}
|
||||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
|
local sum_c_2 = Div{parent=summary,x=2,y=4,width=49}
|
||||||
@@ -442,9 +445,9 @@ local function config_view(display)
|
|||||||
|
|
||||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||||
|
|
||||||
TextBox{parent=summary,x=1,y=2,height=1,text_align=CENTER,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||||
|
|
||||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=51,scroll_height=100,fg_bg=cpair(colors.black,colors.white),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||||
|
|
||||||
local function back_from_settings()
|
local function back_from_settings()
|
||||||
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||||
@@ -466,7 +469,8 @@ local function config_view(display)
|
|||||||
local function save_and_continue()
|
local function save_and_continue()
|
||||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||||
|
|
||||||
if settings.save("reactor-plc.settings") then
|
if settings.save("/reactor-plc.settings") then
|
||||||
|
load_settings(settings_cfg, true)
|
||||||
load_settings(ini_cfg)
|
load_settings(ini_cfg)
|
||||||
|
|
||||||
try_set(networked, ini_cfg.Networked)
|
try_set(networked, ini_cfg.Networked)
|
||||||
@@ -484,6 +488,8 @@ local function config_view(display)
|
|||||||
try_set(path, ini_cfg.LogPath)
|
try_set(path, ini_cfg.LogPath)
|
||||||
try_set(en_dbg, ini_cfg.LogDebug)
|
try_set(en_dbg, ini_cfg.LogDebug)
|
||||||
|
|
||||||
|
tool_ctl.view_cfg.enable()
|
||||||
|
|
||||||
if tool_ctl.importing_legacy then
|
if tool_ctl.importing_legacy then
|
||||||
tool_ctl.importing_legacy = false
|
tool_ctl.importing_legacy = false
|
||||||
sum_pane.set_value(3)
|
sum_pane.set_value(3)
|
||||||
@@ -495,11 +501,11 @@ local function config_view(display)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
PushButton{parent=sum_c_1,x=1,y=14,min_width=6,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=sum_c_1,x=1,y=14,text="\x1b Back",callback=back_from_settings,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=8,y=14,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=cpair(colors.lightGray,colors.white)}
|
||||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=43,y=14,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
TextBox{parent=sum_c_2,x=1,y=1,height=1,text_align=CENTER,text="Settings saved!"}
|
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
|
||||||
|
|
||||||
local function go_home()
|
local function go_home()
|
||||||
main_pane.set_value(1)
|
main_pane.set_value(1)
|
||||||
@@ -511,7 +517,7 @@ local function config_view(display)
|
|||||||
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=sum_c_2,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
PushButton{parent=sum_c_2,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
TextBox{parent=sum_c_3,x=1,y=1,height=2,text_align=CENTER,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
TextBox{parent=sum_c_3,x=1,y=1,height=2,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||||
|
|
||||||
local function delete_legacy()
|
local function delete_legacy()
|
||||||
fs.delete("/reactor-plc/config.lua")
|
fs.delete("/reactor-plc/config.lua")
|
||||||
@@ -521,11 +527,31 @@ local function config_view(display)
|
|||||||
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=sum_c_3,x=1,y=14,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
PushButton{parent=sum_c_3,x=44,y=14,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
TextBox{parent=sum_c_4,x=1,y=1,height=5,text_align=CENTER,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
TextBox{parent=sum_c_4,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||||
|
|
||||||
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
PushButton{parent=sum_c_4,x=1,y=14,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
PushButton{parent=sum_c_4,x=44,y=14,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
|
||||||
|
--#endregion
|
||||||
|
|
||||||
|
-- Config Change Log
|
||||||
|
|
||||||
|
local cl = Div{parent=changelog,x=2,y=4,width=49}
|
||||||
|
|
||||||
|
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
local c_log = ListBox{parent=cl,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||||
|
|
||||||
|
for _, change in ipairs(changes) do
|
||||||
|
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
|
||||||
|
for _, v in ipairs(change[2]) do
|
||||||
|
local e = Div{parent=c_log,height=#util.strwrap(v,46)}
|
||||||
|
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
|
||||||
|
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||||
|
|
||||||
-- set tool functions now that we have the elements
|
-- set tool functions now that we have the elements
|
||||||
|
|
||||||
function tool_ctl.set_networked(enable)
|
function tool_ctl.set_networked(enable)
|
||||||
@@ -593,10 +619,10 @@ local function config_view(display)
|
|||||||
|
|
||||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end
|
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) end
|
||||||
if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
if f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
||||||
if f[1] == "EmerCoolColor" and raw ~= nil then val = color_name_map[raw] end
|
if f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw) end
|
||||||
if val == "nil" then val = "n/a" end
|
if val == "nil" then val = "<not set>" end
|
||||||
|
|
||||||
local c = util.trinary(alternate, cpair(colors.gray,colors.lightGray), cpair(colors.gray,colors.white))
|
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||||
alternate = not alternate
|
alternate = not alternate
|
||||||
|
|
||||||
if string.len(val) > val_max_w then
|
if string.len(val) > val_max_w then
|
||||||
@@ -628,12 +654,12 @@ local function reset_term()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- run the reactor PLC configurator
|
-- run the reactor PLC configurator
|
||||||
---@param ask_config? boolean indicate if this is being called by the PLC startup app due to an invalid configuration
|
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
|
||||||
function configurator.configure(ask_config)
|
function configurator.configure(ask_config)
|
||||||
tool_ctl.ask_config = ask_config == true
|
tool_ctl.ask_config = ask_config == true
|
||||||
tool_ctl.has_config = settings.load("/reactor-plc.settings")
|
|
||||||
|
|
||||||
load_settings(ini_cfg)
|
load_settings(settings_cfg, true)
|
||||||
|
tool_ctl.has_config = load_settings(ini_cfg)
|
||||||
|
|
||||||
reset_term()
|
reset_term()
|
||||||
|
|
||||||
@@ -651,18 +677,14 @@ function configurator.configure(ask_config)
|
|||||||
|
|
||||||
-- handle event
|
-- handle event
|
||||||
if event == "timer" then
|
if event == "timer" then
|
||||||
-- notify timer callback dispatcher
|
|
||||||
tcd.handle(param1)
|
tcd.handle(param1)
|
||||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||||
-- handle a mouse event
|
|
||||||
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||||
if m_e then display.handle_mouse(m_e) end
|
if m_e then display.handle_mouse(m_e) end
|
||||||
elseif event == "char" or event == "key" or event == "key_up" then
|
elseif event == "char" or event == "key" or event == "key_up" then
|
||||||
-- handle a key event
|
|
||||||
local k_e = core.events.new_key_event(event, param1, param2)
|
local k_e = core.events.new_key_event(event, param1, param2)
|
||||||
if k_e then display.handle_key(k_e) end
|
if k_e then display.handle_key(k_e) end
|
||||||
elseif event == "paste" then
|
elseif event == "paste" then
|
||||||
-- handle a paste event
|
|
||||||
display.handle_paste(param1)
|
display.handle_paste(param1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,17 @@ function plc.load_config()
|
|||||||
|
|
||||||
config.Networked = settings.get("Networked")
|
config.Networked = settings.get("Networked")
|
||||||
config.UnitID = settings.get("UnitID")
|
config.UnitID = settings.get("UnitID")
|
||||||
|
|
||||||
config.EmerCoolEnable = settings.get("EmerCoolEnable")
|
config.EmerCoolEnable = settings.get("EmerCoolEnable")
|
||||||
config.EmerCoolSide = settings.get("EmerCoolSide")
|
config.EmerCoolSide = settings.get("EmerCoolSide")
|
||||||
config.EmerCoolColor = settings.get("EmerCoolColor")
|
config.EmerCoolColor = settings.get("EmerCoolColor")
|
||||||
|
|
||||||
config.SVR_Channel = settings.get("SVR_Channel")
|
config.SVR_Channel = settings.get("SVR_Channel")
|
||||||
config.PLC_Channel = settings.get("PLC_Channel")
|
config.PLC_Channel = settings.get("PLC_Channel")
|
||||||
config.ConnTimeout = settings.get("ConnTimeout")
|
config.ConnTimeout = settings.get("ConnTimeout")
|
||||||
config.TrustedRange = settings.get("TrustedRange")
|
config.TrustedRange = settings.get("TrustedRange")
|
||||||
config.AuthKey = settings.get("AuthKey")
|
config.AuthKey = settings.get("AuthKey")
|
||||||
|
|
||||||
config.LogMode = settings.get("LogMode")
|
config.LogMode = settings.get("LogMode")
|
||||||
config.LogPath = settings.get("LogPath")
|
config.LogPath = settings.get("LogPath")
|
||||||
config.LogDebug = settings.get("LogDebug")
|
config.LogDebug = settings.get("LogDebug")
|
||||||
@@ -54,14 +57,24 @@ function plc.load_config()
|
|||||||
cfv.assert_type_bool(config.Networked)
|
cfv.assert_type_bool(config.Networked)
|
||||||
cfv.assert_type_int(config.UnitID)
|
cfv.assert_type_int(config.UnitID)
|
||||||
cfv.assert_type_bool(config.EmerCoolEnable)
|
cfv.assert_type_bool(config.EmerCoolEnable)
|
||||||
cfv.assert_channel(config.SVR_Channel)
|
|
||||||
cfv.assert_channel(config.PLC_Channel)
|
if config.Networked == true then
|
||||||
cfv.assert_type_int(config.ConnTimeout)
|
cfv.assert_channel(config.SVR_Channel)
|
||||||
cfv.assert_min(config.ConnTimeout, 2)
|
cfv.assert_channel(config.PLC_Channel)
|
||||||
cfv.assert_type_num(config.TrustedRange)
|
cfv.assert_type_num(config.ConnTimeout)
|
||||||
cfv.assert_min(config.TrustedRange, 0)
|
cfv.assert_min(config.ConnTimeout, 2)
|
||||||
cfv.assert_type_str(config.AuthKey)
|
cfv.assert_type_num(config.TrustedRange)
|
||||||
|
cfv.assert_min(config.TrustedRange, 0)
|
||||||
|
cfv.assert_type_str(config.AuthKey)
|
||||||
|
|
||||||
|
if type(config.AuthKey) == "string" then
|
||||||
|
local len = string.len(config.AuthKey)
|
||||||
|
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
cfv.assert_type_int(config.LogMode)
|
cfv.assert_type_int(config.LogMode)
|
||||||
|
cfv.assert_range(config.LogMode, 0, 1)
|
||||||
cfv.assert_type_str(config.LogPath)
|
cfv.assert_type_str(config.LogPath)
|
||||||
cfv.assert_type_bool(config.LogDebug)
|
cfv.assert_type_bool(config.LogDebug)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
|
|||||||
local renderer = require("reactor-plc.renderer")
|
local renderer = require("reactor-plc.renderer")
|
||||||
local threads = require("reactor-plc.threads")
|
local threads = require("reactor-plc.threads")
|
||||||
|
|
||||||
local R_PLC_VERSION = "v1.6.0"
|
local R_PLC_VERSION = "v1.6.11"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
@@ -31,7 +31,7 @@ if not plc.load_config() then
|
|||||||
-- try to reconfigure (user action)
|
-- try to reconfigure (user action)
|
||||||
local success, error = configure.configure(true)
|
local success, error = configure.configure(true)
|
||||||
if success then
|
if success then
|
||||||
assert(plc.load_config(), "failed to load valid reactor PLC configuration")
|
assert(plc.load_config(), "failed to load valid configuration")
|
||||||
else
|
else
|
||||||
assert(success, "reactor PLC configuration error: " .. error)
|
assert(success, "reactor PLC configuration error: " .. error)
|
||||||
end
|
end
|
||||||
@@ -50,7 +50,7 @@ log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
|
|||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
|
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
|
||||||
|
|
||||||
crash.set_env("plc", R_PLC_VERSION)
|
crash.set_env("reactor-plc", R_PLC_VERSION)
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- main application
|
-- main application
|
||||||
@@ -69,7 +69,7 @@ local function main()
|
|||||||
ppm.mount_all()
|
ppm.mount_all()
|
||||||
|
|
||||||
-- message authentication init
|
-- message authentication init
|
||||||
if string.len(config.AuthKey) > 0 then
|
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||||
network.init_mac(config.AuthKey)
|
network.init_mac(config.AuthKey)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ function threads.thread__main(smem, init)
|
|||||||
plc_state.degraded = true
|
plc_state.degraded = true
|
||||||
elseif networked and type == "modem" then
|
elseif networked and type == "modem" then
|
||||||
-- we only care if this is our wireless modem
|
-- we only care if this is our wireless modem
|
||||||
if nic.is_modem(device) then
|
-- note, check init_ok first since nic will be nil if it is false
|
||||||
|
if plc_state.init_ok and nic.is_modem(device) then
|
||||||
nic.disconnect()
|
nic.disconnect()
|
||||||
|
|
||||||
println_ts("comms modem disconnected!")
|
println_ts("comms modem disconnected!")
|
||||||
@@ -193,7 +194,7 @@ function threads.thread__main(smem, init)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
log.warning("non-comms modem disconnected")
|
log.warning("a modem was disconnected")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -235,7 +236,8 @@ function threads.thread__main(smem, init)
|
|||||||
rps.reset()
|
rps.reset()
|
||||||
end
|
end
|
||||||
elseif networked and type == "modem" then
|
elseif networked and type == "modem" then
|
||||||
if device.isWireless() and not nic.is_connected() then
|
-- note, check init_ok first since nic will be nil if it is false
|
||||||
|
if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
|
||||||
-- reconnected modem
|
-- reconnected modem
|
||||||
plc_dev.modem = device
|
plc_dev.modem = device
|
||||||
plc_state.no_modem = false
|
plc_state.no_modem = false
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
local rsio = require("scada-common.rsio")
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
-- supervisor comms channel
|
|
||||||
config.SVR_CHANNEL = 16240
|
|
||||||
-- RTU/MODBUS comms channel
|
|
||||||
config.RTU_CHANNEL = 16242
|
|
||||||
-- max trusted modem message distance (0 to disable check)
|
|
||||||
config.TRUSTED_RANGE = 0
|
|
||||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
|
||||||
config.COMMS_TIMEOUT = 5
|
|
||||||
-- facility authentication key (do NOT use one of your passwords)
|
|
||||||
-- this enables verifying that messages are authentic
|
|
||||||
-- all devices on the same network must use the same key
|
|
||||||
-- config.AUTH_KEY = "SCADAfacility123"
|
|
||||||
|
|
||||||
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
|
|
||||||
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
|
|
||||||
config.SOUNDER_VOLUME = 1.0
|
|
||||||
|
|
||||||
-- log path
|
|
||||||
config.LOG_PATH = "/log.txt"
|
|
||||||
-- log mode
|
|
||||||
-- 0 = APPEND (adds to existing file on start)
|
|
||||||
-- 1 = NEW (replaces existing file on start)
|
|
||||||
config.LOG_MODE = 0
|
|
||||||
-- true to log verbose debug messages
|
|
||||||
config.LOG_DEBUG = false
|
|
||||||
|
|
||||||
-- RTU peripheral devices (named: side/network device name)
|
|
||||||
config.RTU_DEVICES = {
|
|
||||||
{
|
|
||||||
name = "boilerValve_0",
|
|
||||||
index = 1,
|
|
||||||
for_reactor = 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name = "turbineValve_0",
|
|
||||||
index = 1,
|
|
||||||
for_reactor = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-- RTU redstone interface definitions
|
|
||||||
config.RTU_REDSTONE = {
|
|
||||||
-- {
|
|
||||||
-- for_reactor = 1,
|
|
||||||
-- io = {
|
|
||||||
-- {
|
|
||||||
-- port = rsio.IO.WASTE_PO,
|
|
||||||
-- side = "top",
|
|
||||||
-- bundled_color = colors.red
|
|
||||||
-- },
|
|
||||||
-- {
|
|
||||||
-- port = rsio.IO.WASTE_PU,
|
|
||||||
-- side = "top",
|
|
||||||
-- bundled_color = colors.orange
|
|
||||||
-- },
|
|
||||||
-- {
|
|
||||||
-- port = rsio.IO.WASTE_POPL,
|
|
||||||
-- side = "top",
|
|
||||||
-- bundled_color = colors.yellow
|
|
||||||
-- },
|
|
||||||
-- {
|
|
||||||
-- port = rsio.IO.WASTE_AM,
|
|
||||||
-- side = "top",
|
|
||||||
-- bundled_color = colors.lime
|
|
||||||
-- }
|
|
||||||
-- }
|
|
||||||
-- }
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
1531
rtu/configure.lua
Normal file
1531
rtu/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -109,12 +109,10 @@ local function init(panel, units)
|
|||||||
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
|
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
|
||||||
|
|
||||||
-- unit name identifier (type + index)
|
-- unit name identifier (type + index)
|
||||||
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.index)
|
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end
|
||||||
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1}
|
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15,height=1}
|
||||||
|
|
||||||
name_box.register(databus.ps, "unit_type_" .. i, function (t)
|
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end)
|
||||||
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- assignment (unit # or facility)
|
-- assignment (unit # or facility)
|
||||||
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
|
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
|
||||||
|
|||||||
69
rtu/rtu.lua
69
rtu/rtu.lua
@@ -5,7 +5,6 @@ local log = require("scada-common.log")
|
|||||||
local types = require("scada-common.types")
|
local types = require("scada-common.types")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("rtu.config")
|
|
||||||
local databus = require("rtu.databus")
|
local databus = require("rtu.databus")
|
||||||
local modbus = require("rtu.modbus")
|
local modbus = require("rtu.modbus")
|
||||||
|
|
||||||
@@ -17,6 +16,57 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
|||||||
local MGMT_TYPE = comms.MGMT_TYPE
|
local MGMT_TYPE = comms.MGMT_TYPE
|
||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
|
|
||||||
|
---@type rtu_config
|
||||||
|
local config = {}
|
||||||
|
|
||||||
|
rtu.config = config
|
||||||
|
|
||||||
|
-- load the RTU configuration
|
||||||
|
function rtu.load_config()
|
||||||
|
if not settings.load("/rtu.settings") then return false end
|
||||||
|
|
||||||
|
config.Peripherals = settings.get("Peripherals")
|
||||||
|
config.Redstone = settings.get("Redstone")
|
||||||
|
|
||||||
|
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||||
|
config.SVR_Channel = settings.get("SVR_Channel")
|
||||||
|
config.RTU_Channel = settings.get("RTU_Channel")
|
||||||
|
config.ConnTimeout = settings.get("ConnTimeout")
|
||||||
|
config.TrustedRange = settings.get("TrustedRange")
|
||||||
|
config.AuthKey = settings.get("AuthKey")
|
||||||
|
config.LogMode = settings.get("LogMode")
|
||||||
|
config.LogPath = settings.get("LogPath")
|
||||||
|
config.LogDebug = settings.get("LogDebug")
|
||||||
|
|
||||||
|
local cfv = util.new_validator()
|
||||||
|
|
||||||
|
cfv.assert_type_num(config.SpeakerVolume)
|
||||||
|
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||||
|
|
||||||
|
cfv.assert_channel(config.SVR_Channel)
|
||||||
|
cfv.assert_channel(config.RTU_Channel)
|
||||||
|
cfv.assert_type_num(config.ConnTimeout)
|
||||||
|
cfv.assert_min(config.ConnTimeout, 2)
|
||||||
|
cfv.assert_type_num(config.TrustedRange)
|
||||||
|
cfv.assert_min(config.TrustedRange, 0)
|
||||||
|
cfv.assert_type_str(config.AuthKey)
|
||||||
|
|
||||||
|
if type(config.AuthKey) == "string" then
|
||||||
|
local len = string.len(config.AuthKey)
|
||||||
|
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
cfv.assert_type_int(config.LogMode)
|
||||||
|
cfv.assert_range(config.LogMode, 0, 1)
|
||||||
|
cfv.assert_type_str(config.LogPath)
|
||||||
|
cfv.assert_type_bool(config.LogDebug)
|
||||||
|
|
||||||
|
cfv.assert_type_table(config.Peripherals)
|
||||||
|
cfv.assert_type_table(config.Redstone)
|
||||||
|
|
||||||
|
return cfv.valid()
|
||||||
|
end
|
||||||
|
|
||||||
-- create a new RTU unit
|
-- create a new RTU unit
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function rtu.init_unit()
|
function rtu.init_unit()
|
||||||
@@ -175,7 +225,7 @@ function rtu.init_sounder(speaker)
|
|||||||
function spkr_ctl.continue()
|
function spkr_ctl.continue()
|
||||||
if spkr_ctl.playing then
|
if spkr_ctl.playing then
|
||||||
if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
|
if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
|
||||||
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME)
|
local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SpeakerVolume)
|
||||||
if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
|
if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -203,11 +253,8 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param version string RTU version
|
---@param version string RTU version
|
||||||
---@param nic nic network interface device
|
---@param nic nic network interface device
|
||||||
---@param rtu_channel integer PLC comms channel
|
|
||||||
---@param svr_channel integer supervisor server channel
|
|
||||||
---@param range integer trusted device connection range
|
|
||||||
---@param conn_watchdog watchdog watchdog reference
|
---@param conn_watchdog watchdog watchdog reference
|
||||||
function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
function rtu.comms(version, nic, conn_watchdog)
|
||||||
local self = {
|
local self = {
|
||||||
sv_addr = comms.BROADCAST,
|
sv_addr = comms.BROADCAST,
|
||||||
seq_num = 0,
|
seq_num = 0,
|
||||||
@@ -218,13 +265,13 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
|||||||
|
|
||||||
local insert = table.insert
|
local insert = table.insert
|
||||||
|
|
||||||
comms.set_trusted_range(range)
|
comms.set_trusted_range(config.TrustedRange)
|
||||||
|
|
||||||
-- PRIVATE FUNCTIONS --
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
-- configure modem channels
|
-- configure modem channels
|
||||||
nic.closeAll()
|
nic.closeAll()
|
||||||
nic.open(rtu_channel)
|
nic.open(config.RTU_Channel)
|
||||||
|
|
||||||
-- send a scada management packet
|
-- send a scada management packet
|
||||||
---@param msg_type MGMT_TYPE
|
---@param msg_type MGMT_TYPE
|
||||||
@@ -236,7 +283,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
|||||||
m_pkt.make(msg_type, msg)
|
m_pkt.make(msg_type, msg)
|
||||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||||
|
|
||||||
nic.transmit(svr_channel, rtu_channel, s_pkt)
|
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
|
||||||
self.seq_num = self.seq_num + 1
|
self.seq_num = self.seq_num + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -280,7 +327,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
|||||||
function public.send_modbus(m_pkt)
|
function public.send_modbus(m_pkt)
|
||||||
local s_pkt = comms.scada_packet()
|
local s_pkt = comms.scada_packet()
|
||||||
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||||
nic.transmit(svr_channel, rtu_channel, s_pkt)
|
nic.transmit(config.SVR_Channel, config.RTU_Channel, s_pkt)
|
||||||
self.seq_num = self.seq_num + 1
|
self.seq_num = self.seq_num + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -365,7 +412,7 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
|||||||
local l_chan = packet.scada_frame.local_channel()
|
local l_chan = packet.scada_frame.local_channel()
|
||||||
local src_addr = packet.scada_frame.src_addr()
|
local src_addr = packet.scada_frame.src_addr()
|
||||||
|
|
||||||
if l_chan == rtu_channel then
|
if l_chan == config.RTU_Channel then
|
||||||
-- check sequence number
|
-- check sequence number
|
||||||
if self.r_seq_num == nil then
|
if self.r_seq_num == nil then
|
||||||
self.r_seq_num = packet.scada_frame.seq_num()
|
self.r_seq_num = packet.scada_frame.seq_num()
|
||||||
|
|||||||
365
rtu/startup.lua
365
rtu/startup.lua
@@ -15,7 +15,7 @@ local rsio = require("scada-common.rsio")
|
|||||||
local types = require("scada-common.types")
|
local types = require("scada-common.types")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("rtu.config")
|
local configure = require("rtu.configure")
|
||||||
local databus = require("rtu.databus")
|
local databus = require("rtu.databus")
|
||||||
local modbus = require("rtu.modbus")
|
local modbus = require("rtu.modbus")
|
||||||
local renderer = require("rtu.renderer")
|
local renderer = require("rtu.renderer")
|
||||||
@@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
|
|||||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||||
|
|
||||||
local RTU_VERSION = "v1.6.6"
|
local RTU_VERSION = "v1.7.13"
|
||||||
|
|
||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||||
@@ -40,27 +40,26 @@ local println = util.println
|
|||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- config validation
|
-- get configuration
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
local cfv = util.new_validator()
|
if not rtu.load_config() then
|
||||||
|
-- try to reconfigure (user action)
|
||||||
|
local success, error = configure.configure(true)
|
||||||
|
if success then
|
||||||
|
assert(rtu.load_config(), "failed to load valid configuration")
|
||||||
|
else
|
||||||
|
assert(success, "RTU configuration error: " .. error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
cfv.assert_channel(config.SVR_CHANNEL)
|
local config = rtu.config
|
||||||
cfv.assert_channel(config.RTU_CHANNEL)
|
|
||||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
|
||||||
cfv.assert_type_num(config.COMMS_TIMEOUT)
|
|
||||||
cfv.assert_min(config.COMMS_TIMEOUT, 2)
|
|
||||||
cfv.assert_type_str(config.LOG_PATH)
|
|
||||||
cfv.assert_type_int(config.LOG_MODE)
|
|
||||||
cfv.assert_type_table(config.RTU_DEVICES)
|
|
||||||
cfv.assert_type_table(config.RTU_REDSTONE)
|
|
||||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING rtu.startup " .. RTU_VERSION)
|
log.info("BOOTING rtu.startup " .. RTU_VERSION)
|
||||||
@@ -85,8 +84,8 @@ local function main()
|
|||||||
ppm.mount_all()
|
ppm.mount_all()
|
||||||
|
|
||||||
-- message authentication init
|
-- message authentication init
|
||||||
if type(config.AUTH_KEY) == "string" then
|
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||||
network.init_mac(config.AUTH_KEY)
|
network.init_mac(config.AuthKey)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- get modem
|
-- get modem
|
||||||
@@ -139,173 +138,174 @@ local function main()
|
|||||||
|
|
||||||
local units = __shared_memory.rtu_sys.units
|
local units = __shared_memory.rtu_sys.units
|
||||||
|
|
||||||
local rtu_redstone = config.RTU_REDSTONE
|
local rtu_redstone = config.Redstone
|
||||||
local rtu_devices = config.RTU_DEVICES
|
local rtu_devices = config.Peripherals
|
||||||
|
|
||||||
-- configure RTU gateway based on config file definitions
|
-- configure RTU gateway based on settings file definitions
|
||||||
local function configure()
|
local function sys_config()
|
||||||
-- redstone interfaces
|
-- redstone interfaces
|
||||||
|
local rs_rtus = {}
|
||||||
|
|
||||||
|
-- go through redstone definitions list
|
||||||
for entry_idx = 1, #rtu_redstone do
|
for entry_idx = 1, #rtu_redstone do
|
||||||
local rs_rtu = redstone_rtu.new()
|
local entry = rtu_redstone[entry_idx] ---@type rtu_rs_definition
|
||||||
local io_table = rtu_redstone[entry_idx].io ---@type table
|
local assignment
|
||||||
local io_reactor = rtu_redstone[entry_idx].for_reactor ---@type integer
|
local for_reactor = entry.unit
|
||||||
|
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
|
||||||
|
|
||||||
-- CHECK: reactor ID must be >= to 1
|
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
|
||||||
if (not util.is_int(io_reactor)) or (io_reactor < 0) then
|
---@cast for_reactor integer
|
||||||
local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0")
|
assignment = "reactor unit " .. entry.unit
|
||||||
println(message)
|
if rs_rtus[for_reactor] == nil then
|
||||||
log.fatal(message)
|
log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
|
||||||
return false
|
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
|
||||||
end
|
|
||||||
|
|
||||||
-- CHECK: io table exists
|
|
||||||
if type(io_table) ~= "table" then
|
|
||||||
local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
|
|
||||||
println(message)
|
|
||||||
log.fatal(message)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local capabilities = {}
|
|
||||||
|
|
||||||
log.debug(util.c("configure> starting redstone RTU I/O linking for reactor ", io_reactor, "..."))
|
|
||||||
|
|
||||||
local continue = true
|
|
||||||
|
|
||||||
-- CHECK: no duplicate entries
|
|
||||||
for i = 1, #units do
|
|
||||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
|
||||||
if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
|
|
||||||
-- duplicate entry
|
|
||||||
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
|
|
||||||
" with already defined redstone I/O")
|
|
||||||
println(message)
|
|
||||||
log.warning(message)
|
|
||||||
continue = false
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
|
elseif entry.unit == nil then
|
||||||
|
assignment = "facility"
|
||||||
|
for_reactor = 0
|
||||||
|
if rs_rtus[for_reactor] == nil then
|
||||||
|
log.debug(util.c("sys_config> allocated redstone RTU for the facility"))
|
||||||
|
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
|
||||||
|
println(message)
|
||||||
|
log.fatal(message)
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- not a duplicate
|
-- verify configuration
|
||||||
if continue then
|
local valid = false
|
||||||
for i = 1, #io_table do
|
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
|
||||||
local valid = false
|
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
|
||||||
local conf = io_table[i]
|
end
|
||||||
|
|
||||||
-- verify configuration
|
local rs_rtu = rs_rtus[for_reactor].rtu
|
||||||
if rsio.is_valid_port(conf.port) and rsio.is_valid_side(conf.side) then
|
local capabilities = rs_rtus[for_reactor].capabilities
|
||||||
if conf.bundled_color then
|
|
||||||
valid = rsio.is_color(conf.bundled_color)
|
|
||||||
else
|
|
||||||
valid = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not valid then
|
if not valid then
|
||||||
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
|
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
|
||||||
" (for reactor ", io_reactor, ")")
|
println(message)
|
||||||
|
log.fatal(message)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
-- link redstone in RTU
|
||||||
|
local mode = rsio.get_io_mode(entry.port)
|
||||||
|
if mode == rsio.IO_MODE.DIGITAL_IN then
|
||||||
|
-- can't have duplicate inputs
|
||||||
|
if util.table_contains(capabilities, entry.port) then
|
||||||
|
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
|
||||||
println(message)
|
println(message)
|
||||||
log.fatal(message)
|
log.warning(message)
|
||||||
return false
|
|
||||||
else
|
else
|
||||||
-- link redstone in RTU
|
rs_rtu.link_di(entry.side, entry.color)
|
||||||
local mode = rsio.get_io_mode(conf.port)
|
|
||||||
if mode == rsio.IO_MODE.DIGITAL_IN then
|
|
||||||
-- can't have duplicate inputs
|
|
||||||
if util.table_contains(capabilities, conf.port) then
|
|
||||||
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
|
|
||||||
println(message)
|
|
||||||
log.warning(message)
|
|
||||||
else
|
|
||||||
rs_rtu.link_di(conf.side, conf.bundled_color)
|
|
||||||
end
|
|
||||||
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
|
||||||
rs_rtu.link_do(conf.side, conf.bundled_color)
|
|
||||||
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
|
||||||
-- can't have duplicate inputs
|
|
||||||
if util.table_contains(capabilities, conf.port) then
|
|
||||||
local message = util.c("configure> skipping duplicate input for port ", rsio.to_string(conf.port), " on side ", conf.side)
|
|
||||||
println(message)
|
|
||||||
log.warning(message)
|
|
||||||
else
|
|
||||||
rs_rtu.link_ai(conf.side)
|
|
||||||
end
|
|
||||||
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
|
||||||
rs_rtu.link_ao(conf.side)
|
|
||||||
else
|
|
||||||
-- should be unreachable code, we already validated ports
|
|
||||||
log.error("configure> fell through if chain attempting to identify IO mode", true)
|
|
||||||
println("configure> encountered a software error, check logs")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(capabilities, conf.port)
|
|
||||||
|
|
||||||
log.debug(util.c("configure> linked redstone ", #capabilities, ": ", rsio.to_string(conf.port),
|
|
||||||
" (", conf.side, ") for reactor ", io_reactor))
|
|
||||||
end
|
end
|
||||||
|
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
|
||||||
|
rs_rtu.link_do(entry.side, entry.color)
|
||||||
|
elseif mode == rsio.IO_MODE.ANALOG_IN then
|
||||||
|
-- can't have duplicate inputs
|
||||||
|
if util.table_contains(capabilities, entry.port) then
|
||||||
|
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name)
|
||||||
|
println(message)
|
||||||
|
log.warning(message)
|
||||||
|
else
|
||||||
|
rs_rtu.link_ai(entry.side)
|
||||||
|
end
|
||||||
|
elseif mode == rsio.IO_MODE.ANALOG_OUT then
|
||||||
|
rs_rtu.link_ao(entry.side)
|
||||||
|
else
|
||||||
|
-- should be unreachable code, we already validated ports
|
||||||
|
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true)
|
||||||
|
println("sys_config> encountered a software error, check logs")
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class rtu_unit_registry_entry
|
table.insert(capabilities, entry.port)
|
||||||
local unit = {
|
|
||||||
uid = 0, ---@type integer
|
|
||||||
name = "redstone_io", ---@type string
|
|
||||||
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
|
|
||||||
index = entry_idx, ---@type integer
|
|
||||||
reactor = io_reactor, ---@type integer
|
|
||||||
device = capabilities, ---@type table use device field for redstone ports
|
|
||||||
is_multiblock = false, ---@type boolean
|
|
||||||
formed = nil, ---@type boolean|nil
|
|
||||||
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
|
|
||||||
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
|
|
||||||
modbus_io = modbus.new(rs_rtu, false),
|
|
||||||
pkt_queue = nil, ---@type mqueue|nil
|
|
||||||
thread = nil ---@type parallel_thread|nil
|
|
||||||
}
|
|
||||||
|
|
||||||
table.insert(units, unit)
|
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment))
|
||||||
|
|
||||||
local for_message = "facility"
|
|
||||||
if io_reactor > 0 then
|
|
||||||
for_message = util.c("reactor ", io_reactor)
|
|
||||||
end
|
|
||||||
|
|
||||||
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
|
||||||
|
|
||||||
unit.uid = #units
|
|
||||||
|
|
||||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- create unit entries for redstone RTUs
|
||||||
|
for for_reactor, def in pairs(rs_rtus) do
|
||||||
|
---@class rtu_unit_registry_entry
|
||||||
|
local unit = {
|
||||||
|
uid = 0, ---@type integer
|
||||||
|
name = "redstone_io", ---@type string
|
||||||
|
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
|
||||||
|
index = false, ---@type integer|false
|
||||||
|
reactor = for_reactor, ---@type integer
|
||||||
|
device = def.capabilities, ---@type table use device field for redstone ports
|
||||||
|
is_multiblock = false, ---@type boolean
|
||||||
|
formed = nil, ---@type boolean|nil
|
||||||
|
hw_state = RTU_UNIT_HW_STATE.OK, ---@type RTU_UNIT_HW_STATE
|
||||||
|
rtu = def.rtu, ---@type rtu_device|rtu_rs_device
|
||||||
|
modbus_io = modbus.new(def.rtu, false),
|
||||||
|
pkt_queue = nil, ---@type mqueue|nil
|
||||||
|
thread = nil ---@type parallel_thread|nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table.insert(units, unit)
|
||||||
|
|
||||||
|
local for_message = "facility"
|
||||||
|
if util.is_int(for_reactor) then
|
||||||
|
for_message = util.c("reactor unit ", for_reactor)
|
||||||
|
end
|
||||||
|
|
||||||
|
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
||||||
|
|
||||||
|
unit.uid = #units
|
||||||
|
|
||||||
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||||
|
end
|
||||||
|
|
||||||
-- mounted peripherals
|
-- mounted peripherals
|
||||||
for i = 1, #rtu_devices do
|
for i = 1, #rtu_devices do
|
||||||
local name = rtu_devices[i].name
|
local entry = rtu_devices[i] ---@type rtu_peri_definition
|
||||||
local index = rtu_devices[i].index
|
local name = entry.name
|
||||||
local for_reactor = rtu_devices[i].for_reactor
|
local index = entry.index
|
||||||
|
local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit)
|
||||||
|
|
||||||
-- CHECK: name is a string
|
-- CHECK: name is a string
|
||||||
if type(name) ~= "string" then
|
if type(name) ~= "string" then
|
||||||
local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string")
|
local message = util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")
|
||||||
println(message)
|
println(message)
|
||||||
log.fatal(message)
|
log.fatal(message)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- CHECK: index is an integer >= 1
|
-- CHECK: index type
|
||||||
if (not util.is_int(index)) or (index <= 0) then
|
if (index ~= nil) and (not util.is_int(index)) then
|
||||||
local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")
|
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")
|
||||||
println(message)
|
println(message)
|
||||||
log.fatal(message)
|
log.fatal(message)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- CHECK: index range
|
||||||
|
local function validate_index(min, max)
|
||||||
|
if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then
|
||||||
|
local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min)
|
||||||
|
if max ~= nil then message = util.c(message, " and <= ", max) end
|
||||||
|
println(message)
|
||||||
|
log.fatal(message)
|
||||||
|
return false
|
||||||
|
else return true end
|
||||||
|
end
|
||||||
|
|
||||||
-- CHECK: reactor is an integer >= 0
|
-- CHECK: reactor is an integer >= 0
|
||||||
if (not util.is_int(for_reactor)) or (for_reactor < 0) then
|
local function validate_assign(for_facility)
|
||||||
local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0")
|
if for_facility and for_reactor ~= 0 then
|
||||||
println(message)
|
local message = util.c("sys_config> device entry #", i, ": must only be for the facility")
|
||||||
log.fatal(message)
|
println(message)
|
||||||
return false
|
log.fatal(message)
|
||||||
|
return false
|
||||||
|
elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then
|
||||||
|
local message = util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")
|
||||||
|
println(message)
|
||||||
|
log.fatal(message)
|
||||||
|
return false
|
||||||
|
else return true end
|
||||||
end
|
end
|
||||||
|
|
||||||
local device = ppm.get_periph(name)
|
local device = ppm.get_periph(name)
|
||||||
@@ -318,7 +318,7 @@ local function main()
|
|||||||
local faulted = nil ---@type boolean|nil
|
local faulted = nil ---@type boolean|nil
|
||||||
|
|
||||||
if device == nil then
|
if device == nil then
|
||||||
local message = util.c("configure> '", name, "' not found, using placeholder")
|
local message = util.c("sys_config> '", name, "' not found, using placeholder")
|
||||||
println(message)
|
println(message)
|
||||||
log.warning(message)
|
log.warning(message)
|
||||||
|
|
||||||
@@ -330,70 +330,93 @@ local function main()
|
|||||||
|
|
||||||
if type == "boilerValve" then
|
if type == "boilerValve" then
|
||||||
-- boiler multiblock
|
-- boiler multiblock
|
||||||
|
if not validate_index(1, 2) then return false end
|
||||||
|
if not validate_assign() then return false end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
|
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||||
rtu_iface, faulted = boilerv_rtu.new(device)
|
rtu_iface, faulted = boilerv_rtu.new(device)
|
||||||
is_multiblock = true
|
is_multiblock = true
|
||||||
formed = device.isFormed()
|
formed = device.isFormed()
|
||||||
|
|
||||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock"))
|
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock"))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
elseif type == "turbineValve" then
|
elseif type == "turbineValve" then
|
||||||
-- turbine multiblock
|
-- turbine multiblock
|
||||||
|
if not validate_index(1, 3) then return false end
|
||||||
|
if not validate_assign() then return false end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
|
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||||
rtu_iface, faulted = turbinev_rtu.new(device)
|
rtu_iface, faulted = turbinev_rtu.new(device)
|
||||||
is_multiblock = true
|
is_multiblock = true
|
||||||
formed = device.isFormed()
|
formed = device.isFormed()
|
||||||
|
|
||||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
|
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock"))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
elseif type == "dynamicValve" then
|
elseif type == "dynamicValve" then
|
||||||
-- dynamic tank multiblock
|
-- dynamic tank multiblock
|
||||||
|
if entry.unit == nil then
|
||||||
|
if not validate_index(1, 4) then return false end
|
||||||
|
if not validate_assign(true) then return false end
|
||||||
|
else
|
||||||
|
if not validate_index(1, 1) then return false end
|
||||||
|
if not validate_assign() then return false end
|
||||||
|
end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||||
rtu_iface, faulted = dynamicv_rtu.new(device)
|
rtu_iface, faulted = dynamicv_rtu.new(device)
|
||||||
is_multiblock = true
|
is_multiblock = true
|
||||||
formed = device.isFormed()
|
formed = device.isFormed()
|
||||||
|
|
||||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
elseif type == "inductionPort" then
|
elseif type == "inductionPort" then
|
||||||
-- induction matrix multiblock
|
-- induction matrix multiblock
|
||||||
|
if not validate_assign(true) then return false end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
||||||
rtu_iface, faulted = imatrix_rtu.new(device)
|
rtu_iface, faulted = imatrix_rtu.new(device)
|
||||||
is_multiblock = true
|
is_multiblock = true
|
||||||
formed = device.isFormed()
|
formed = device.isFormed()
|
||||||
|
|
||||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock"))
|
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock"))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
elseif type == "spsPort" then
|
elseif type == "spsPort" then
|
||||||
-- SPS multiblock
|
-- SPS multiblock
|
||||||
|
if not validate_assign(true) then return false end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.SPS
|
rtu_type = RTU_UNIT_TYPE.SPS
|
||||||
rtu_iface, faulted = sps_rtu.new(device)
|
rtu_iface, faulted = sps_rtu.new(device)
|
||||||
is_multiblock = true
|
is_multiblock = true
|
||||||
formed = device.isFormed()
|
formed = device.isFormed()
|
||||||
|
|
||||||
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
|
||||||
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
|
println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
|
||||||
log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock"))
|
log.fatal(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock"))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
elseif type == "solarNeutronActivator" then
|
elseif type == "solarNeutronActivator" then
|
||||||
-- SNA
|
-- SNA
|
||||||
|
if not validate_assign() then return false end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.SNA
|
rtu_type = RTU_UNIT_TYPE.SNA
|
||||||
rtu_iface, faulted = sna_rtu.new(device)
|
rtu_iface, faulted = sna_rtu.new(device)
|
||||||
elseif type == "environmentDetector" then
|
elseif type == "environmentDetector" then
|
||||||
-- advanced peripherals environment detector
|
-- advanced peripherals environment detector
|
||||||
|
if not validate_index(1) then return false end
|
||||||
|
if not validate_assign(entry.unit == nil) then return false end
|
||||||
|
|
||||||
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||||
rtu_iface, faulted = envd_rtu.new(device)
|
rtu_iface, faulted = envd_rtu.new(device)
|
||||||
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
||||||
@@ -401,7 +424,7 @@ local function main()
|
|||||||
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
||||||
rtu_iface = rtu.init_unit().interface()
|
rtu_iface = rtu.init_unit().interface()
|
||||||
else
|
else
|
||||||
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
|
local message = util.c("sys_config> device '", name, "' is not a known type (", type, ")")
|
||||||
println_ts(message)
|
println_ts(message)
|
||||||
log.fatal(message)
|
log.fatal(message)
|
||||||
return false
|
return false
|
||||||
@@ -409,12 +432,12 @@ local function main()
|
|||||||
|
|
||||||
if is_multiblock then
|
if is_multiblock then
|
||||||
if not formed then
|
if not formed then
|
||||||
log.info(util.c("configure> device '", name, "' is not formed"))
|
log.info(util.c("sys_config> device '", name, "' is not formed"))
|
||||||
elseif faulted then
|
elseif faulted then
|
||||||
-- sometimes there is a race condition on server boot where it reports formed, but
|
-- sometimes there is a race condition on server boot where it reports formed, but
|
||||||
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
|
-- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later
|
||||||
formed = false
|
formed = false
|
||||||
log.warning(util.c("configure> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
|
log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -423,7 +446,7 @@ local function main()
|
|||||||
uid = 0, ---@type integer
|
uid = 0, ---@type integer
|
||||||
name = name, ---@type string
|
name = name, ---@type string
|
||||||
type = rtu_type, ---@type RTU_UNIT_TYPE
|
type = rtu_type, ---@type RTU_UNIT_TYPE
|
||||||
index = index, ---@type integer
|
index = index or false, ---@type integer|false
|
||||||
reactor = for_reactor, ---@type integer
|
reactor = for_reactor, ---@type integer
|
||||||
device = device, ---@type table
|
device = device, ---@type table
|
||||||
is_multiblock = is_multiblock, ---@type boolean
|
is_multiblock = is_multiblock, ---@type boolean
|
||||||
@@ -444,7 +467,7 @@ local function main()
|
|||||||
for_message = util.c("reactor ", for_reactor)
|
for_message = util.c("reactor ", for_reactor)
|
||||||
end
|
end
|
||||||
|
|
||||||
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
|
log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
|
||||||
|
|
||||||
rtu_unit.uid = #units
|
rtu_unit.uid = #units
|
||||||
|
|
||||||
@@ -465,7 +488,6 @@ local function main()
|
|||||||
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
|
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- we made it through all that trusting-user-to-write-a-config-file chaos
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -475,9 +497,9 @@ local function main()
|
|||||||
|
|
||||||
local rtu_state = __shared_memory.rtu_state
|
local rtu_state = __shared_memory.rtu_state
|
||||||
|
|
||||||
log.debug("boot> running configure()")
|
log.debug("boot> running sys_config()")
|
||||||
|
|
||||||
if configure() then
|
if sys_config() then
|
||||||
-- start UI
|
-- start UI
|
||||||
local message
|
local message
|
||||||
rtu_state.fp_ok, message = renderer.try_start_ui(units)
|
rtu_state.fp_ok, message = renderer.try_start_ui(units)
|
||||||
@@ -502,12 +524,11 @@ local function main()
|
|||||||
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
|
databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
|
||||||
|
|
||||||
-- start connection watchdog
|
-- start connection watchdog
|
||||||
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
|
||||||
log.debug("startup> conn watchdog started")
|
log.debug("startup> conn watchdog started")
|
||||||
|
|
||||||
-- setup comms
|
-- setup comms
|
||||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, config.RTU_CHANNEL, config.SVR_CHANNEL,
|
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||||
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
|
|
||||||
log.debug("startup> comms init")
|
log.debug("startup> comms init")
|
||||||
|
|
||||||
-- init threads
|
-- init threads
|
||||||
|
|||||||
238
rtu/threads.lua
238
rtu/threads.lua
@@ -28,6 +28,147 @@ local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
|||||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||||
|
|
||||||
|
---@param smem rtu_shared_memory
|
||||||
|
---@param println_ts function
|
||||||
|
---@param iface string
|
||||||
|
---@param type string
|
||||||
|
---@param device table
|
||||||
|
---@param unit rtu_unit_registry_entry
|
||||||
|
local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
|
||||||
|
local sys = smem.rtu_sys
|
||||||
|
|
||||||
|
-- find disconnected device to reconnect
|
||||||
|
-- note: cannot check isFormed as that would yield this coroutine and consume events
|
||||||
|
if unit.name == iface then
|
||||||
|
local resend_advert, faulted, unknown, invalid = false, false, false, false
|
||||||
|
|
||||||
|
local function fail(msg)
|
||||||
|
invalid = true
|
||||||
|
log.error(msg .. " in config")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- found, re-link
|
||||||
|
unit.device = device
|
||||||
|
|
||||||
|
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||||
|
resend_advert = true
|
||||||
|
if type == "boilerValve" then
|
||||||
|
-- boiler multiblock
|
||||||
|
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("boiler '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
||||||
|
if (unit.index == false) or unit.index < 1 or unit.index > 2 then fail(util.c("boiler '", unit.name, "' cannot init, invalid index provided")) end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||||
|
elseif type == "turbineValve" then
|
||||||
|
-- turbine multiblock
|
||||||
|
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("turbine '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
||||||
|
if (unit.index == false) or unit.index < 1 or unit.index > 3 then fail(util.c("turbine '", unit.name, "' cannot init, invalid index provided")) end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||||
|
elseif type == "dynamicValve" then
|
||||||
|
-- dynamic tank multiblock
|
||||||
|
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("dynamic tank '", unit.name, "' cannot init, no valid assignment provided")) end
|
||||||
|
|
||||||
|
if (unit.reactor == 0 and ((unit.index == false) or unit.index < 1 or unit.index > 4)) or
|
||||||
|
(unit.reactor > 0 and unit.index ~= 1) then
|
||||||
|
fail(util.c("dynamic tank '", unit.name, "' cannot init, invalid index provided"))
|
||||||
|
end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
||||||
|
elseif type == "inductionPort" then
|
||||||
|
-- induction matrix multiblock
|
||||||
|
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.IMATRIX
|
||||||
|
elseif type == "spsPort" then
|
||||||
|
-- SPS multiblock
|
||||||
|
if unit.reactor ~= 0 then fail(util.c("SPS '", unit.name, "' cannot init, not assigned to facility")) end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.SPS
|
||||||
|
elseif type == "solarNeutronActivator" then
|
||||||
|
-- SNA
|
||||||
|
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.SNA
|
||||||
|
elseif type == "environmentDetector" then
|
||||||
|
-- advanced peripherals environment detector
|
||||||
|
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
|
||||||
|
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
|
||||||
|
|
||||||
|
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||||
|
else
|
||||||
|
resend_advert = false
|
||||||
|
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
||||||
|
end
|
||||||
|
|
||||||
|
databus.tx_unit_hw_type(unit.uid, unit.type)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if disconnected on startup, config wouldn't have been validated
|
||||||
|
-- checking now that it has connected; the config isn't valid, so don't connect it
|
||||||
|
if invalid then
|
||||||
|
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
||||||
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
unit.formed = util.trinary(faulted, false, nil)
|
||||||
|
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||||
|
unit.rtu, faulted = turbinev_rtu.new(device)
|
||||||
|
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)
|
||||||
|
unit.formed = util.trinary(faulted, false, nil)
|
||||||
|
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
||||||
|
unit.rtu, faulted = sps_rtu.new(device)
|
||||||
|
unit.formed = util.trinary(faulted, false, nil)
|
||||||
|
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
||||||
|
unit.rtu, faulted = sna_rtu.new(device)
|
||||||
|
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||||
|
unit.rtu, faulted = envd_rtu.new(device)
|
||||||
|
else
|
||||||
|
unknown = true
|
||||||
|
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
if unit.is_multiblock then
|
||||||
|
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
||||||
|
if unit.formed == false then
|
||||||
|
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
||||||
|
end
|
||||||
|
elseif faulted then
|
||||||
|
unit.hw_state = UNIT_HW_STATE.FAULTED
|
||||||
|
elseif not unknown then
|
||||||
|
unit.hw_state = UNIT_HW_STATE.OK
|
||||||
|
else
|
||||||
|
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
||||||
|
end
|
||||||
|
|
||||||
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
||||||
|
|
||||||
|
if not unknown then
|
||||||
|
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||||
|
|
||||||
|
local type_name = types.rtu_type_to_string(unit.type)
|
||||||
|
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
|
||||||
|
println_ts(message)
|
||||||
|
log.info(message)
|
||||||
|
|
||||||
|
if resend_advert then
|
||||||
|
sys.rtu_comms.send_advertisement(sys.units)
|
||||||
|
else
|
||||||
|
sys.rtu_comms.send_remounted(unit.uid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- main thread
|
-- main thread
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param smem rtu_shared_memory
|
---@param smem rtu_shared_memory
|
||||||
@@ -180,102 +321,7 @@ function threads.thread__main(smem)
|
|||||||
else
|
else
|
||||||
-- relink lost peripheral to correct unit entry
|
-- relink lost peripheral to correct unit entry
|
||||||
for i = 1, #units do
|
for i = 1, #units do
|
||||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
handle_unit_mount(smem, println_ts, param1, type, device, units[i])
|
||||||
|
|
||||||
-- find disconnected device to reconnect
|
|
||||||
-- note: cannot check isFormed as that would yield this coroutine and consume events
|
|
||||||
if unit.name == param1 then
|
|
||||||
local resend_advert = false
|
|
||||||
local faulted = false
|
|
||||||
local unknown = false
|
|
||||||
|
|
||||||
-- found, re-link
|
|
||||||
unit.device = device
|
|
||||||
|
|
||||||
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
|
||||||
resend_advert = true
|
|
||||||
if type == "boilerValve" then
|
|
||||||
-- boiler multiblock
|
|
||||||
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
|
|
||||||
elseif type == "turbineValve" then
|
|
||||||
-- turbine multiblock
|
|
||||||
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
|
|
||||||
elseif type == "inductionPort" then
|
|
||||||
-- induction matrix multiblock
|
|
||||||
unit.type = RTU_UNIT_TYPE.IMATRIX
|
|
||||||
elseif type == "spsPort" then
|
|
||||||
-- SPS multiblock
|
|
||||||
unit.type = RTU_UNIT_TYPE.SPS
|
|
||||||
elseif type == "solarNeutronActivator" then
|
|
||||||
-- SNA
|
|
||||||
unit.type = RTU_UNIT_TYPE.SNA
|
|
||||||
elseif type == "environmentDetector" then
|
|
||||||
-- advanced peripherals environment detector
|
|
||||||
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
|
|
||||||
else
|
|
||||||
resend_advert = false
|
|
||||||
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
|
||||||
unit.formed = util.trinary(faulted, false, nil)
|
|
||||||
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
|
||||||
unit.rtu, faulted = turbinev_rtu.new(device)
|
|
||||||
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)
|
|
||||||
unit.formed = util.trinary(faulted, false, nil)
|
|
||||||
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
|
||||||
unit.rtu, faulted = sps_rtu.new(device)
|
|
||||||
unit.formed = util.trinary(faulted, false, nil)
|
|
||||||
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
|
||||||
unit.rtu, faulted = sna_rtu.new(device)
|
|
||||||
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
|
||||||
unit.rtu, faulted = envd_rtu.new(device)
|
|
||||||
else
|
|
||||||
unknown = true
|
|
||||||
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
|
||||||
end
|
|
||||||
|
|
||||||
if unit.is_multiblock then
|
|
||||||
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
|
||||||
if unit.formed == false then
|
|
||||||
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
|
||||||
end
|
|
||||||
elseif faulted then
|
|
||||||
unit.hw_state = UNIT_HW_STATE.FAULTED
|
|
||||||
elseif not unknown then
|
|
||||||
unit.hw_state = UNIT_HW_STATE.OK
|
|
||||||
else
|
|
||||||
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
|
||||||
end
|
|
||||||
|
|
||||||
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
|
||||||
|
|
||||||
if not unknown then
|
|
||||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
|
||||||
|
|
||||||
local type_name = types.rtu_type_to_string(unit.type)
|
|
||||||
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
|
|
||||||
println_ts(message)
|
|
||||||
log.info(message)
|
|
||||||
|
|
||||||
if resend_advert then
|
|
||||||
rtu_comms.send_advertisement(units)
|
|
||||||
else
|
|
||||||
rtu_comms.send_remounted(unit.uid)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ local max_distance = nil
|
|||||||
local comms = {}
|
local comms = {}
|
||||||
|
|
||||||
-- protocol/data version (protocol/data independent changes tracked by util.lua version)
|
-- protocol/data version (protocol/data independent changes tracked by util.lua version)
|
||||||
comms.version = "2.4.1"
|
comms.version = "2.4.4"
|
||||||
|
|
||||||
---@enum PROTOCOL
|
---@enum PROTOCOL
|
||||||
local PROTOCOL = {
|
local PROTOCOL = {
|
||||||
@@ -311,7 +311,7 @@ function comms.authd_packet()
|
|||||||
self.valid = false
|
self.valid = false
|
||||||
self.raw = self.modem_msg_in.msg
|
self.raw = self.modem_msg_in.msg
|
||||||
|
|
||||||
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
|
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
|
||||||
-- outside of maximum allowable transmission distance
|
-- outside of maximum allowable transmission distance
|
||||||
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ local MODE = { APPEND = 0, NEW = 1 }
|
|||||||
log.MODE = MODE
|
log.MODE = MODE
|
||||||
|
|
||||||
local logger = {
|
local logger = {
|
||||||
|
not_ready = true,
|
||||||
path = "/log.txt",
|
path = "/log.txt",
|
||||||
mode = MODE.APPEND,
|
mode = MODE.APPEND,
|
||||||
debug = false,
|
debug = false,
|
||||||
@@ -32,6 +33,8 @@ local free_space = fs.getFreeSpace
|
|||||||
-- private log write function
|
-- private log write function
|
||||||
---@param msg string
|
---@param msg string
|
||||||
local function _log(msg)
|
local function _log(msg)
|
||||||
|
if logger.not_ready then return end
|
||||||
|
|
||||||
local out_of_space = false
|
local out_of_space = false
|
||||||
local time_stamp = os.date("[%c] ")
|
local time_stamp = os.date("[%c] ")
|
||||||
local stamped = time_stamp .. util.strval(msg)
|
local stamped = time_stamp .. util.strval(msg)
|
||||||
@@ -94,6 +97,8 @@ function log.init(path, write_mode, include_debug, dmesg_redirect)
|
|||||||
else
|
else
|
||||||
logger.dmesg_out = term.current()
|
logger.dmesg_out = term.current()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
logger.not_ready = false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- close the log file handle
|
-- close the log file handle
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ local log = require("scada-common.log")
|
|||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local md5 = require("lockbox.digest.md5")
|
local md5 = require("lockbox.digest.md5")
|
||||||
local sha256 = require("lockbox.digest.sha2_256")
|
local sha1 = require("lockbox.digest.sha1")
|
||||||
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
||||||
local hmac = require("lockbox.mac.hmac")
|
local hmac = require("lockbox.mac.hmac")
|
||||||
local stream = require("lockbox.util.stream")
|
local stream = require("lockbox.util.stream")
|
||||||
@@ -31,12 +31,12 @@ function network.init_mac(passkey)
|
|||||||
local key_deriv = pbkdf2()
|
local key_deriv = pbkdf2()
|
||||||
|
|
||||||
-- setup PBKDF2
|
-- setup PBKDF2
|
||||||
key_deriv.setPassword(passkey)
|
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha1))
|
||||||
|
key_deriv.setBlockLen(20)
|
||||||
|
key_deriv.setDKeyLen(20)
|
||||||
|
key_deriv.setIterations(256)
|
||||||
key_deriv.setSalt("pepper")
|
key_deriv.setSalt("pepper")
|
||||||
key_deriv.setIterations(32)
|
key_deriv.setPassword(passkey)
|
||||||
key_deriv.setBlockLen(8)
|
|
||||||
key_deriv.setDKeyLen(16)
|
|
||||||
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256))
|
|
||||||
key_deriv.finish()
|
key_deriv.finish()
|
||||||
|
|
||||||
c_eng.key = array.fromHex(key_deriv.asHex())
|
c_eng.key = array.fromHex(key_deriv.asHex())
|
||||||
|
|||||||
@@ -161,10 +161,10 @@ local function peri_init(iface)
|
|||||||
|
|
||||||
setmetatable(self.device, mt)
|
setmetatable(self.device, mt)
|
||||||
|
|
||||||
return {
|
---@class ppm_entry
|
||||||
type = self.type,
|
local entry = { type = self.type, dev = self.device }
|
||||||
dev = self.device
|
|
||||||
}
|
return entry
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------
|
----------------------
|
||||||
@@ -310,7 +310,11 @@ function ppm.list_avail() return peripheral.getNames() end
|
|||||||
-- list mounted peripherals
|
-- list mounted peripherals
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return table mounts
|
---@return table mounts
|
||||||
function ppm.list_mounts() return ppm_sys.mounts end
|
function ppm.list_mounts()
|
||||||
|
local list = {}
|
||||||
|
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
-- get a mounted peripheral side/interface by device table
|
-- get a mounted peripheral side/interface by device table
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -417,4 +421,15 @@ function ppm.get_monitor_list()
|
|||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- HELPER FUNCTIONS
|
||||||
|
|
||||||
|
-- get the block size of a monitor given its width and height <b>at a text scale of 0.5</b>
|
||||||
|
---@nodiscard
|
||||||
|
---@param width integer character width
|
||||||
|
---@param height integer character height
|
||||||
|
---@return integer block_width, integer block_height
|
||||||
|
function ppm.monitor_block_size(width, height)
|
||||||
|
return math.floor((width - 15) / 21) + 1, math.floor((height - 10) / 14) + 1
|
||||||
|
end
|
||||||
|
|
||||||
return ppm
|
return ppm
|
||||||
|
|||||||
@@ -82,46 +82,89 @@ rsio.IO_LVL = IO_LVL
|
|||||||
rsio.IO_DIR = IO_DIR
|
rsio.IO_DIR = IO_DIR
|
||||||
rsio.IO_MODE = IO_MODE
|
rsio.IO_MODE = IO_MODE
|
||||||
rsio.IO = IO_PORT
|
rsio.IO = IO_PORT
|
||||||
|
rsio.NUM_PORTS = IO_PORT.U_EMER_COOL
|
||||||
|
|
||||||
|
-- self checks
|
||||||
|
|
||||||
|
local dup_chk = {}
|
||||||
|
for _, v in pairs(IO_PORT) do
|
||||||
|
assert(dup_chk[v] ~= true, "duplicate in port list")
|
||||||
|
dup_chk[v] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
--#region Utility Functions
|
--#region Utility Functions
|
||||||
|
|
||||||
|
local PORT_NAMES = {
|
||||||
|
"F_SCRAM",
|
||||||
|
"F_ACK",
|
||||||
|
"R_SCRAM",
|
||||||
|
"R_RESET",
|
||||||
|
"R_ENABLE",
|
||||||
|
"U_ACK",
|
||||||
|
"F_ALARM",
|
||||||
|
"F_ALARM_ANY",
|
||||||
|
"WASTE_PU",
|
||||||
|
"WASTE_PO",
|
||||||
|
"WASTE_POPL",
|
||||||
|
"WASTE_AM",
|
||||||
|
"R_ACTIVE",
|
||||||
|
"R_AUTO_CTRL",
|
||||||
|
"R_SCRAMMED",
|
||||||
|
"R_AUTO_SCRAM",
|
||||||
|
"R_HIGH_DMG",
|
||||||
|
"R_HIGH_TEMP",
|
||||||
|
"R_LOW_COOLANT",
|
||||||
|
"R_EXCESS_HC",
|
||||||
|
"R_EXCESS_WS",
|
||||||
|
"R_INSUFF_FUEL",
|
||||||
|
"R_PLC_FAULT",
|
||||||
|
"R_PLC_TIMEOUT",
|
||||||
|
"U_ALARM",
|
||||||
|
"U_EMER_COOL"
|
||||||
|
}
|
||||||
|
|
||||||
|
local MODES = {
|
||||||
|
IO_MODE.DIGITAL_IN, -- F_SCRAM
|
||||||
|
IO_MODE.DIGITAL_IN, -- F_ACK
|
||||||
|
IO_MODE.DIGITAL_IN, -- R_SCRAM
|
||||||
|
IO_MODE.DIGITAL_IN, -- R_RESET
|
||||||
|
IO_MODE.DIGITAL_IN, -- R_ENABLE
|
||||||
|
IO_MODE.DIGITAL_IN, -- U_ACK
|
||||||
|
IO_MODE.DIGITAL_OUT, -- F_ALARM
|
||||||
|
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY
|
||||||
|
IO_MODE.DIGITAL_OUT, -- WASTE_PU
|
||||||
|
IO_MODE.DIGITAL_OUT, -- WASTE_PO
|
||||||
|
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
|
||||||
|
IO_MODE.DIGITAL_OUT, -- WASTE_AM
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
|
||||||
|
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
|
||||||
|
IO_MODE.DIGITAL_OUT, -- U_ALARM
|
||||||
|
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
|
||||||
|
assert(rsio.NUM_PORTS == #MODES, "modes length incorrect")
|
||||||
|
|
||||||
-- port to string
|
-- port to string
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param port IO_PORT
|
---@param port IO_PORT
|
||||||
function rsio.to_string(port)
|
function rsio.to_string(port)
|
||||||
local names = {
|
if util.is_int(port) and port > 0 and port <= #PORT_NAMES then
|
||||||
"F_SCRAM",
|
return PORT_NAMES[port]
|
||||||
"F_ACK",
|
|
||||||
"R_SCRAM",
|
|
||||||
"R_RESET",
|
|
||||||
"R_ENABLE",
|
|
||||||
"U_ACK",
|
|
||||||
"F_ALARM",
|
|
||||||
"F_ALARM_ANY",
|
|
||||||
"WASTE_PU",
|
|
||||||
"WASTE_PO",
|
|
||||||
"WASTE_POPL",
|
|
||||||
"WASTE_AM",
|
|
||||||
"R_ACTIVE",
|
|
||||||
"R_AUTO_CTRL",
|
|
||||||
"R_SCRAMMED",
|
|
||||||
"R_AUTO_SCRAM",
|
|
||||||
"R_HIGH_DMG",
|
|
||||||
"R_HIGH_TEMP",
|
|
||||||
"R_LOW_COOLANT",
|
|
||||||
"R_EXCESS_HC",
|
|
||||||
"R_EXCESS_WS",
|
|
||||||
"R_INSUFF_FUEL",
|
|
||||||
"R_PLC_FAULT",
|
|
||||||
"R_PLC_TIMEOUT",
|
|
||||||
"U_ALARM",
|
|
||||||
"U_EMER_COOL"
|
|
||||||
}
|
|
||||||
|
|
||||||
if util.is_int(port) and port > 0 and port <= #names then
|
|
||||||
return names[port]
|
|
||||||
else
|
else
|
||||||
return "UNKNOWN"
|
return "UNKNOWN"
|
||||||
end
|
end
|
||||||
@@ -196,45 +239,24 @@ local RS_DIO_MAP = {
|
|||||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
|
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
|
||||||
|
|
||||||
|
-- get the I/O direction of a port
|
||||||
|
---@nodiscard
|
||||||
|
---@param port IO_PORT
|
||||||
|
---@return IO_DIR
|
||||||
|
function rsio.get_io_dir(port)
|
||||||
|
if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode
|
||||||
|
else return IO_DIR.IN end
|
||||||
|
end
|
||||||
|
|
||||||
-- get the mode of a port
|
-- get the mode of a port
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param port IO_PORT
|
---@param port IO_PORT
|
||||||
---@return IO_MODE
|
---@return IO_MODE
|
||||||
function rsio.get_io_mode(port)
|
function rsio.get_io_mode(port)
|
||||||
local modes = {
|
if rsio.is_valid_port(port) then return MODES[port]
|
||||||
IO_MODE.DIGITAL_IN, -- F_SCRAM
|
else return IO_MODE.ANALOG_IN end
|
||||||
IO_MODE.DIGITAL_IN, -- F_ACK
|
|
||||||
IO_MODE.DIGITAL_IN, -- R_SCRAM
|
|
||||||
IO_MODE.DIGITAL_IN, -- R_RESET
|
|
||||||
IO_MODE.DIGITAL_IN, -- R_ENABLE
|
|
||||||
IO_MODE.DIGITAL_IN, -- U_ACK
|
|
||||||
IO_MODE.DIGITAL_OUT, -- F_ALARM
|
|
||||||
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY
|
|
||||||
IO_MODE.DIGITAL_OUT, -- WASTE_PU
|
|
||||||
IO_MODE.DIGITAL_OUT, -- WASTE_PO
|
|
||||||
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
|
|
||||||
IO_MODE.DIGITAL_OUT, -- WASTE_AM
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
|
|
||||||
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
|
|
||||||
IO_MODE.DIGITAL_OUT, -- U_ALARM
|
|
||||||
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
|
|
||||||
}
|
|
||||||
|
|
||||||
if util.is_int(port) and port > 0 and port <= #modes then
|
|
||||||
return modes[port]
|
|
||||||
else
|
|
||||||
return IO_MODE.ANALOG_IN
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
@@ -248,7 +270,7 @@ local RS_SIDES = rs.getSides()
|
|||||||
---@param port IO_PORT
|
---@param port IO_PORT
|
||||||
---@return boolean valid
|
---@return boolean valid
|
||||||
function rsio.is_valid_port(port)
|
function rsio.is_valid_port(port)
|
||||||
return util.is_int(port) and (port > 0) and (port <= IO_PORT.U_EMER_COOL)
|
return util.is_int(port) and port > 0 and port <= rsio.NUM_PORTS
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check if a side is valid
|
-- check if a side is valid
|
||||||
@@ -266,12 +288,24 @@ end
|
|||||||
|
|
||||||
-- check if a color is a valid single color
|
-- check if a color is a valid single color
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param color integer
|
---@param color any
|
||||||
---@return boolean valid
|
---@return boolean valid
|
||||||
function rsio.is_color(color)
|
function rsio.is_color(color)
|
||||||
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0)
|
return util.is_int(color) and (color > 0) and (_B_AND(color, (color - 1)) == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- color to string
|
||||||
|
---@nodiscard
|
||||||
|
---@param color color
|
||||||
|
---@return string
|
||||||
|
function rsio.color_name(color)
|
||||||
|
local color_name_map = { [colors.red] = "red", [colors.orange] = "orange", [colors.yellow] = "yellow", [colors.lime] = "lime", [colors.green] = "green", [colors.cyan] = "cyan", [colors.lightBlue] = "lightBlue", [colors.blue] = "blue", [colors.purple] = "purple", [colors.magenta] = "magenta", [colors.pink] = "pink", [colors.white] = "white", [colors.lightGray] = "lightGray", [colors.gray] = "gray", [colors.black] = "black", [colors.brown] = "brown" }
|
||||||
|
|
||||||
|
if rsio.is_color(color) then
|
||||||
|
return color_name_map[color]
|
||||||
|
else return "unknown" end
|
||||||
|
end
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
--#region Digital I/O
|
--#region Digital I/O
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
|
|||||||
|
|
||||||
---@class rtu_advertisement
|
---@class rtu_advertisement
|
||||||
---@field type RTU_UNIT_TYPE
|
---@field type RTU_UNIT_TYPE
|
||||||
---@field index integer
|
---@field index integer|false
|
||||||
---@field reactor integer
|
---@field reactor integer
|
||||||
---@field rsio table|nil
|
---@field rsio table|nil
|
||||||
|
|
||||||
@@ -252,6 +252,14 @@ types.ALARM_STATE_NAMES = {
|
|||||||
-- STRING TYPES --
|
-- STRING TYPES --
|
||||||
--#region
|
--#region
|
||||||
|
|
||||||
|
---@alias side
|
||||||
|
---|"top"
|
||||||
|
---|"bottom"
|
||||||
|
---|"left"
|
||||||
|
---|"right"
|
||||||
|
---|"front"
|
||||||
|
---|"back"
|
||||||
|
|
||||||
---@alias os_event
|
---@alias os_event
|
||||||
---| "alarm"
|
---| "alarm"
|
||||||
---| "char"
|
---| "char"
|
||||||
|
|||||||
@@ -4,19 +4,30 @@
|
|||||||
|
|
||||||
local cc_strings = require("cc.strings")
|
local cc_strings = require("cc.strings")
|
||||||
|
|
||||||
|
local math = math
|
||||||
|
local string = string
|
||||||
|
local table = table
|
||||||
|
local os = os
|
||||||
|
|
||||||
|
local getmetatable = getmetatable
|
||||||
|
local print = print
|
||||||
|
local tostring = tostring
|
||||||
|
local type = type
|
||||||
|
|
||||||
|
local t_concat = table.concat
|
||||||
|
local t_insert = table.insert
|
||||||
|
local t_pack = table.pack
|
||||||
|
|
||||||
---@class util
|
---@class util
|
||||||
local util = {}
|
local util = {}
|
||||||
|
|
||||||
-- scada-common version
|
-- scada-common version
|
||||||
util.version = "1.1.3"
|
util.version = "1.1.14"
|
||||||
|
|
||||||
-- ENVIRONMENT CONSTANTS --
|
|
||||||
|
|
||||||
util.TICK_TIME_S = 0.05
|
util.TICK_TIME_S = 0.05
|
||||||
util.TICK_TIME_MS = 50
|
util.TICK_TIME_MS = 50
|
||||||
|
|
||||||
-- OPERATORS --
|
--#region OPERATORS
|
||||||
--#region
|
|
||||||
|
|
||||||
-- trinary operator
|
-- trinary operator
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -30,37 +41,29 @@ end
|
|||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- PRINT --
|
--#region PRINT
|
||||||
--#region
|
|
||||||
|
local p_time = "[%H:%M:%S] "
|
||||||
|
|
||||||
-- print
|
-- print
|
||||||
---@param message any
|
---@param message any
|
||||||
function util.print(message)
|
function util.print(message) term.write(tostring(message)) end
|
||||||
term.write(tostring(message))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- print line
|
-- print line
|
||||||
---@param message any
|
---@param message any
|
||||||
function util.println(message)
|
function util.println(message) print(tostring(message)) end
|
||||||
print(tostring(message))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- timestamped print
|
-- timestamped print
|
||||||
---@param message any
|
---@param message any
|
||||||
function util.print_ts(message)
|
function util.print_ts(message) term.write(os.date(p_time) .. tostring(message)) end
|
||||||
term.write(os.date("[%H:%M:%S] ") .. tostring(message))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- timestamped print line
|
-- timestamped print line
|
||||||
---@param message any
|
---@param message any
|
||||||
function util.println_ts(message)
|
function util.println_ts(message) print(os.date(p_time) .. tostring(message)) end
|
||||||
print(os.date("[%H:%M:%S] ") .. tostring(message))
|
|
||||||
end
|
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- STRING TOOLS --
|
--#region STRING TOOLS
|
||||||
--#region
|
|
||||||
|
|
||||||
-- get a value as a string
|
-- get a value as a string
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -68,21 +71,29 @@ end
|
|||||||
---@return string
|
---@return string
|
||||||
function util.strval(val)
|
function util.strval(val)
|
||||||
local t = type(val)
|
local t = type(val)
|
||||||
|
if t == "string" then return val end
|
||||||
-- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables)
|
-- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables)
|
||||||
if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then
|
if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then
|
||||||
return "[" .. tostring(val) .. "]"
|
return t_concat{"[", tostring(val), "]"}
|
||||||
else
|
else return tostring(val) end
|
||||||
return tostring(val)
|
end
|
||||||
end
|
|
||||||
|
-- tokenize a string by a separator<br>
|
||||||
|
-- does not behave exactly like C's strtok
|
||||||
|
---@param str string string to tokenize
|
||||||
|
---@param sep string separator to tokenize by
|
||||||
|
---@return table token_list
|
||||||
|
function util.strtok(str, sep)
|
||||||
|
local list = {}
|
||||||
|
for part in string.gmatch(str, "([^" .. sep .. "]+)") do t_insert(list, part) end
|
||||||
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
-- repeat a space n times
|
-- repeat a space n times
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param n integer
|
---@param n integer
|
||||||
---@return string
|
---@return string
|
||||||
function util.spaces(n)
|
function util.spaces(n) return string.rep(" ", n) end
|
||||||
return string.rep(" ", n)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- pad text to a minimum width
|
-- pad text to a minimum width
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -94,7 +105,7 @@ function util.pad(str, n)
|
|||||||
local lpad = math.floor((n - len) / 2)
|
local lpad = math.floor((n - len) / 2)
|
||||||
local rpad = (n - len) - lpad
|
local rpad = (n - len) - lpad
|
||||||
|
|
||||||
return util.spaces(lpad) .. str .. util.spaces(rpad)
|
return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- wrap a string into a table of lines
|
-- wrap a string into a table of lines
|
||||||
@@ -104,19 +115,14 @@ end
|
|||||||
---@return table lines
|
---@return table lines
|
||||||
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
|
function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
|
||||||
|
|
||||||
-- luacheck: no unused args
|
|
||||||
|
|
||||||
-- concatenation with built-in to string
|
-- concatenation with built-in to string
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@vararg any
|
---@vararg any
|
||||||
---@return string
|
---@return string
|
||||||
---@diagnostic disable-next-line: unused-vararg
|
|
||||||
function util.concat(...)
|
function util.concat(...)
|
||||||
local str = ""
|
local args, strings = t_pack(...), {}
|
||||||
|
for i = 1, args.n do strings[i] = util.strval(args[i]) end
|
||||||
for _, v in ipairs(arg) do str = str .. util.strval(v) end
|
return t_concat(strings)
|
||||||
|
|
||||||
return str
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- alias
|
-- alias
|
||||||
@@ -126,12 +132,7 @@ util.c = util.concat
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param format string
|
---@param format string
|
||||||
---@vararg any
|
---@vararg any
|
||||||
---@diagnostic disable-next-line: unused-vararg
|
function util.sprintf(format, ...) return string.format(format, ...) end
|
||||||
function util.sprintf(format, ...)
|
|
||||||
return string.format(format, table.unpack(arg))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- luacheck: unused args
|
|
||||||
|
|
||||||
-- format a number string with commas as the thousands separator<br>
|
-- format a number string with commas as the thousands separator<br>
|
||||||
-- subtracts from spaces at the start if present for each comma used
|
-- subtracts from spaces at the start if present for each comma used
|
||||||
@@ -144,7 +145,7 @@ function util.comma_format(num)
|
|||||||
local i = 1
|
local i = 1
|
||||||
|
|
||||||
while i > 0 do
|
while i > 0 do
|
||||||
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2')
|
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", "%1,%2")
|
||||||
if i > 0 then commas = commas + 1 end
|
if i > 0 then commas = commas + 1 end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -158,31 +159,24 @@ end
|
|||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- MATH --
|
--#region MATH
|
||||||
--#region
|
|
||||||
|
|
||||||
-- is a value an integer
|
-- is a value an integer
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param x any value
|
---@param x any value
|
||||||
---@return boolean is_integer if the number is an integer
|
---@return boolean is_integer
|
||||||
function util.is_int(x)
|
function util.is_int(x) return type(x) == "number" and x == math.floor(x) end
|
||||||
return type(x) == "number" and x == math.floor(x)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get the sign of a number
|
-- get the sign of a number
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param x number value
|
---@param x number value
|
||||||
---@return integer sign (-1 for < 0, 1 otherwise)
|
---@return integer sign (-1 for < 0, 1 otherwise)
|
||||||
function util.sign(x)
|
function util.sign(x) return util.trinary(x < 0, -1, 1) end
|
||||||
return util.trinary(x < 0, -1, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- round a number to an integer
|
-- round a number to an integer
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return integer rounded
|
---@return integer rounded
|
||||||
function util.round(x)
|
function util.round(x) return math.floor(x + 0.5) end
|
||||||
return math.floor(x + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get a new moving average object
|
-- get a new moving average object
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -191,7 +185,7 @@ end
|
|||||||
function util.mov_avg(length, default)
|
function util.mov_avg(length, default)
|
||||||
local data = {}
|
local data = {}
|
||||||
local index = 1
|
local index = 1
|
||||||
local last_t = 0 ---@type number|nil
|
local last_t = 0 ---@type number|nil
|
||||||
|
|
||||||
---@class moving_average
|
---@class moving_average
|
||||||
local public = {}
|
local public = {}
|
||||||
@@ -200,16 +194,14 @@ function util.mov_avg(length, default)
|
|||||||
---@param x number value
|
---@param x number value
|
||||||
function public.reset(x)
|
function public.reset(x)
|
||||||
data = {}
|
data = {}
|
||||||
for _ = 1, length do table.insert(data, x) end
|
for _ = 1, length do t_insert(data, x) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- record a new value
|
-- record a new value
|
||||||
---@param x number new value
|
---@param x number new value
|
||||||
---@param t number? optional last update time to prevent duplicated entries
|
---@param t number? optional last update time to prevent duplicated entries
|
||||||
function public.record(x, t)
|
function public.record(x, t)
|
||||||
if type(t) == "number" and last_t == t then
|
if type(t) == "number" and last_t == t then return end
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
data[index] = x
|
data[index] = x
|
||||||
last_t = t
|
last_t = t
|
||||||
@@ -232,23 +224,21 @@ function util.mov_avg(length, default)
|
|||||||
return public
|
return public
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TIME --
|
--#endregion
|
||||||
|
|
||||||
|
--#region TIME
|
||||||
|
|
||||||
-- current time
|
-- current time
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return integer milliseconds
|
---@return integer milliseconds
|
||||||
function util.time_ms()
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
return os.epoch('local')
|
function util.time_ms() return os.epoch("local") end
|
||||||
end
|
|
||||||
|
|
||||||
-- current time
|
-- current time
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return number seconds
|
---@return number seconds
|
||||||
function util.time_s()
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
return os.epoch('local') / 1000.0
|
function util.time_s() return os.epoch("local") / 1000.0 end
|
||||||
end
|
|
||||||
|
|
||||||
-- current time
|
-- current time
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -257,17 +247,14 @@ function util.time() return util.time_ms() end
|
|||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- OS --
|
--#region OS
|
||||||
--#region
|
|
||||||
|
|
||||||
-- OS pull event raw wrapper with types
|
-- OS pull event raw wrapper with types
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param target_event? string event to wait for
|
---@param target_event? string event to wait for
|
||||||
---@return os_event event, any param1, any param2, any param3, any param4, any param5
|
---@return os_event event, any param1, any param2, any param3, any param4, any param5
|
||||||
function util.pull_event(target_event)
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
return os.pullEventRaw(target_event)
|
function util.pull_event(target_event) return os.pullEventRaw(target_event) end
|
||||||
end
|
|
||||||
|
|
||||||
-- OS queue event raw wrapper with types
|
-- OS queue event raw wrapper with types
|
||||||
---@param event os_event
|
---@param event os_event
|
||||||
@@ -285,30 +272,23 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param t number timer duration in seconds
|
---@param t number timer duration in seconds
|
||||||
---@return integer timer ID
|
---@return integer timer ID
|
||||||
function util.start_timer(t)
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
return os.startTimer(t)
|
function util.start_timer(t) return os.startTimer(t) end
|
||||||
end
|
|
||||||
|
|
||||||
-- cancel an OS timer
|
-- cancel an OS timer
|
||||||
---@param timer integer timer ID
|
---@param timer integer timer ID
|
||||||
function util.cancel_timer(timer)
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
os.cancelTimer(timer)
|
function util.cancel_timer(timer) os.cancelTimer(timer) end
|
||||||
end
|
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- PARALLELIZATION --
|
--#region PARALLELIZATION
|
||||||
--#region
|
|
||||||
|
|
||||||
-- protected sleep call so we still are in charge of catching termination
|
-- protected sleep call so we still are in charge of catching termination
|
||||||
---@param t integer seconds
|
---@param t integer seconds
|
||||||
--- EVENT_CONSUMER: this function consumes events
|
--- EVENT_CONSUMER: this function consumes events
|
||||||
function util.psleep(t)
|
|
||||||
---@diagnostic disable-next-line: undefined-field
|
---@diagnostic disable-next-line: undefined-field
|
||||||
pcall(os.sleep, t)
|
function util.psleep(t) pcall(os.sleep, t) end
|
||||||
end
|
|
||||||
|
|
||||||
-- no-op to provide a brief pause (1 tick) to yield<br>
|
-- no-op to provide a brief pause (1 tick) to yield<br>
|
||||||
--- EVENT_CONSUMER: this function consumes events
|
--- EVENT_CONSUMER: this function consumes events
|
||||||
@@ -330,8 +310,7 @@ end
|
|||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- TABLE UTILITIES --
|
--#region TABLE UTILITIES
|
||||||
--#region
|
|
||||||
|
|
||||||
-- delete elements from a table if the passed function returns false when passed a table element<br>
|
-- delete elements from a table if the passed function returns false when passed a table element<br>
|
||||||
-- put briefly: deletes elements that return false, keeps elements that return true
|
-- put briefly: deletes elements that return false, keeps elements that return true
|
||||||
@@ -369,10 +348,19 @@ function util.table_contains(t, element)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- count the length of a table, even if the values are not sequential or contain named keys
|
||||||
|
---@nodiscard
|
||||||
|
---@param t table
|
||||||
|
---@return integer length
|
||||||
|
function util.table_len(t)
|
||||||
|
local n = 0
|
||||||
|
for _, _ in pairs(t) do n = n + 1 end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- MEKANISM POWER --
|
--#region MEKANISM POWER
|
||||||
--#region
|
|
||||||
|
|
||||||
-- convert Joules to FE
|
-- convert Joules to FE
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -401,8 +389,7 @@ local function ZFE(fe) return fe / 1000000000000000000000.0 end -- how & why did
|
|||||||
---@param format? string format override
|
---@param format? string format override
|
||||||
---@return string str, string? unit
|
---@return string str, string? unit
|
||||||
function util.power_format(fe, combine_label, format)
|
function util.power_format(fe, combine_label, format)
|
||||||
local unit
|
local unit, value
|
||||||
local value
|
|
||||||
|
|
||||||
if type(format) ~= "string" then format = "%.2f" end
|
if type(format) ~= "string" then format = "%.2f" end
|
||||||
|
|
||||||
@@ -441,8 +428,7 @@ end
|
|||||||
|
|
||||||
--#endregion
|
--#endregion
|
||||||
|
|
||||||
-- UTILITY CLASSES --
|
--#region UTILITY CLASSES
|
||||||
--#region
|
|
||||||
|
|
||||||
-- WATCHDOG --
|
-- WATCHDOG --
|
||||||
|
|
||||||
@@ -451,32 +437,25 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param timeout number timeout duration
|
---@param timeout number timeout duration
|
||||||
function util.new_watchdog(timeout)
|
function util.new_watchdog(timeout)
|
||||||
local self = {
|
local self = { timeout = timeout, wd_timer = util.start_timer(timeout) }
|
||||||
timeout = timeout,
|
|
||||||
wd_timer = util.start_timer(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class watchdog
|
---@class watchdog
|
||||||
local public = {}
|
local public = {}
|
||||||
|
|
||||||
-- check if a timer is this watchdog
|
-- check if a timer is this watchdog
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param timer number timer event timer ID
|
---@param timer number event timer ID
|
||||||
function public.is_timer(timer) return self.wd_timer == timer end
|
function public.is_timer(timer) return self.wd_timer == timer end
|
||||||
|
|
||||||
-- satiate the beast
|
-- satiate the beast
|
||||||
function public.feed()
|
function public.feed()
|
||||||
if self.wd_timer ~= nil then
|
public.cancel()
|
||||||
util.cancel_timer(self.wd_timer)
|
|
||||||
end
|
|
||||||
self.wd_timer = util.start_timer(self.timeout)
|
self.wd_timer = util.start_timer(self.timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- cancel the watchdog
|
-- cancel the watchdog
|
||||||
function public.cancel()
|
function public.cancel()
|
||||||
if self.wd_timer ~= nil then
|
if self.wd_timer ~= nil then util.cancel_timer(self.wd_timer) end
|
||||||
util.cancel_timer(self.wd_timer)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return public
|
return public
|
||||||
@@ -489,10 +468,7 @@ end
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param period number clock period
|
---@param period number clock period
|
||||||
function util.new_clock(period)
|
function util.new_clock(period)
|
||||||
local self = {
|
local self = { period = period, timer = nil }
|
||||||
period = period,
|
|
||||||
timer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
---@class clock
|
---@class clock
|
||||||
local public = {}
|
local public = {}
|
||||||
@@ -525,6 +501,7 @@ function util.new_validator()
|
|||||||
function public.assert_type_str(value) valid = valid and type(value) == "string" end
|
function public.assert_type_str(value) valid = valid and type(value) == "string" end
|
||||||
function public.assert_type_table(value) valid = valid and type(value) == "table" end
|
function public.assert_type_table(value) valid = valid and type(value) == "table" end
|
||||||
|
|
||||||
|
function public.assert(check) valid = valid and (check == true) end
|
||||||
function public.assert_eq(check, expect) valid = valid and check == expect end
|
function public.assert_eq(check, expect) valid = valid and check == expect end
|
||||||
function public.assert_min(check, min) valid = valid and check >= min end
|
function public.assert_min(check, min) valid = valid and check >= min end
|
||||||
function public.assert_min_ex(check, min) valid = valid and check > min end
|
function public.assert_min_ex(check, min) valid = valid and check > min end
|
||||||
@@ -533,7 +510,7 @@ function util.new_validator()
|
|||||||
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
|
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
|
||||||
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
|
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
|
||||||
|
|
||||||
function public.assert_channel(channel) valid = valid and type(channel) == "number" and channel >= 0 and channel <= 65535 end
|
function public.assert_channel(channel) valid = valid and util.is_int(channel) and channel >= 0 and channel <= 65535 end
|
||||||
|
|
||||||
-- check if all assertions passed successfully
|
-- check if all assertions passed successfully
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
|
|||||||
22
startup.lua
22
startup.lua
@@ -1,30 +1,28 @@
|
|||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local BOOTLOADER_VERSION = "0.3"
|
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
|
||||||
|
local BOOTLOADER_VERSION = "1.0"
|
||||||
|
|
||||||
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
|
println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION)
|
||||||
|
println("BOOT> SCANNING FOR APPLICATIONS...")
|
||||||
|
|
||||||
local exit_code ---@type boolean
|
local exit_code
|
||||||
|
|
||||||
println_ts("BOOT> SCANNING FOR APPLICATIONS...")
|
|
||||||
|
|
||||||
if fs.exists("reactor-plc/startup.lua") then
|
if fs.exists("reactor-plc/startup.lua") then
|
||||||
println("BOOT> FOUND REACTOR PLC CODE: EXEC STARTUP")
|
println("BOOT> EXEC REACTOR PLC STARTUP")
|
||||||
exit_code = shell.execute("reactor-plc/startup")
|
exit_code = shell.execute("reactor-plc/startup")
|
||||||
elseif fs.exists("rtu/startup.lua") then
|
elseif fs.exists("rtu/startup.lua") then
|
||||||
println("BOOT> FOUND RTU CODE: EXEC STARTUP")
|
println("BOOT> EXEC RTU STARTUP")
|
||||||
exit_code = shell.execute("rtu/startup")
|
exit_code = shell.execute("rtu/startup")
|
||||||
elseif fs.exists("supervisor/startup.lua") then
|
elseif fs.exists("supervisor/startup.lua") then
|
||||||
println("BOOT> FOUND SUPERVISOR CODE: EXEC STARTUP")
|
println("BOOT> EXEC SUPERVISOR STARTUP")
|
||||||
exit_code = shell.execute("supervisor/startup")
|
exit_code = shell.execute("supervisor/startup")
|
||||||
elseif fs.exists("coordinator/startup.lua") then
|
elseif fs.exists("coordinator/startup.lua") then
|
||||||
println("BOOT> FOUND COORDINATOR CODE: EXEC STARTUP")
|
println("BOOT> EXEC COORDINATOR STARTUP")
|
||||||
exit_code = shell.execute("coordinator/startup")
|
exit_code = shell.execute("coordinator/startup")
|
||||||
elseif fs.exists("pocket/startup.lua") then
|
elseif fs.exists("pocket/startup.lua") then
|
||||||
println("BOOT> FOUND POCKET CODE: EXEC STARTUP")
|
println("BOOT> EXEC POCKET STARTUP")
|
||||||
exit_code = shell.execute("pocket/startup")
|
exit_code = shell.execute("pocket/startup")
|
||||||
else
|
else
|
||||||
println("BOOT> NO SCADA STARTUP FOUND")
|
println("BOOT> NO SCADA STARTUP FOUND")
|
||||||
@@ -32,6 +30,6 @@ else
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if not exit_code then println_ts("BOOT> APPLICATION CRASHED") end
|
if not exit_code then println("BOOT> APPLICATION CRASHED") end
|
||||||
|
|
||||||
return exit_code
|
return exit_code
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
local config = {}
|
|
||||||
|
|
||||||
-- supervisor comms channel
|
|
||||||
config.SVR_CHANNEL = 16240
|
|
||||||
-- PLC comms channel
|
|
||||||
config.PLC_CHANNEL = 16241
|
|
||||||
-- RTU/MODBUS comms channel
|
|
||||||
config.RTU_CHANNEL = 16242
|
|
||||||
-- coordinator comms channel
|
|
||||||
config.CRD_CHANNEL = 16243
|
|
||||||
-- pocket comms channel
|
|
||||||
config.PKT_CHANNEL = 16244
|
|
||||||
-- max trusted modem message distance
|
|
||||||
-- (0 to disable check)
|
|
||||||
config.TRUSTED_RANGE = 0
|
|
||||||
-- time in seconds (>= 2) before assuming a remote
|
|
||||||
-- device is no longer active
|
|
||||||
config.PLC_TIMEOUT = 5
|
|
||||||
config.RTU_TIMEOUT = 5
|
|
||||||
config.CRD_TIMEOUT = 5
|
|
||||||
config.PKT_TIMEOUT = 5
|
|
||||||
-- facility authentication key
|
|
||||||
-- (do NOT use one of your passwords)
|
|
||||||
-- this enables verifying that messages are authentic
|
|
||||||
-- all devices on this network must use this key
|
|
||||||
-- config.AUTH_KEY = "SCADAfacility123"
|
|
||||||
|
|
||||||
-- expected number of reactors
|
|
||||||
config.NUM_REACTORS = 4
|
|
||||||
-- expected number of devices for each unit
|
|
||||||
config.REACTOR_COOLING = {
|
|
||||||
-- reactor unit 1
|
|
||||||
{ BOILERS = 1, TURBINES = 1, TANK = false },
|
|
||||||
-- reactor unit 2
|
|
||||||
{ BOILERS = 1, TURBINES = 1, TANK = false },
|
|
||||||
-- reactor unit 3
|
|
||||||
{ BOILERS = 1, TURBINES = 1, TANK = false },
|
|
||||||
-- reactor unit 4
|
|
||||||
{ BOILERS = 1, TURBINES = 1, TANK = false }
|
|
||||||
}
|
|
||||||
-- advanced facility dynamic tank configuration
|
|
||||||
-- (see wiki for details)
|
|
||||||
-- by default, dynamic tanks are for each unit
|
|
||||||
config.FAC_TANK_MODE = 0
|
|
||||||
config.FAC_TANK_DEFS = { 0, 0, 0, 0 }
|
|
||||||
|
|
||||||
-- log path
|
|
||||||
config.LOG_PATH = "/log.txt"
|
|
||||||
-- log mode
|
|
||||||
-- 0 = APPEND (adds to existing file on start)
|
|
||||||
-- 1 = NEW (replaces existing file on start)
|
|
||||||
config.LOG_MODE = 0
|
|
||||||
-- true to log verbose debug messages
|
|
||||||
config.LOG_DEBUG = false
|
|
||||||
|
|
||||||
return config
|
|
||||||
1096
supervisor/configure.lua
Normal file
1096
supervisor/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -135,7 +135,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
|
|
||||||
-- create units
|
-- create units
|
||||||
for i = 1, num_reactors do
|
for i = 1, num_reactors do
|
||||||
table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BOILERS, cooling_conf.r_cool[i].TURBINES))
|
table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount))
|
||||||
table.insert(self.group_map, 0)
|
table.insert(self.group_map, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -232,9 +232,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
|
|
||||||
-- link a redstone RTU session
|
-- link a redstone RTU session
|
||||||
---@param rs_unit unit_session
|
---@param rs_unit unit_session
|
||||||
function public.add_redstone(rs_unit)
|
function public.add_redstone(rs_unit) table.insert(self.redstone, rs_unit) end
|
||||||
table.insert(self.redstone, rs_unit)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- link an induction matrix RTU session
|
-- link an induction matrix RTU session
|
||||||
---@param imatrix unit_session
|
---@param imatrix unit_session
|
||||||
@@ -258,23 +256,11 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
|
|
||||||
-- link a dynamic tank RTU session
|
-- link a dynamic tank RTU session
|
||||||
---@param dynamic_tank unit_session
|
---@param dynamic_tank unit_session
|
||||||
---@return boolean linked dynamic tank accepted (max 1)
|
function public.add_tank(dynamic_tank) table.insert(self.tanks, dynamic_tank) end
|
||||||
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
|
-- link an environment detector RTU session
|
||||||
---@param envd unit_session
|
---@param envd unit_session
|
||||||
---@return boolean linked environment detector accepted (max 1)
|
function public.add_envd(envd) table.insert(self.envd, envd) end
|
||||||
function public.add_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
|
-- purge devices associated with the given RTU session ID
|
||||||
---@param session integer RTU session ID
|
---@param session integer RTU session ID
|
||||||
@@ -351,7 +337,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
if state_changed then
|
if state_changed then
|
||||||
self.saturated = false
|
self.saturated = false
|
||||||
|
|
||||||
log.debug("FAC: state changed from " .. PROCESS_NAMES[self.last_mode + 1] .. " to " .. PROCESS_NAMES[self.mode + 1])
|
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
|
||||||
|
|
||||||
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
|
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
|
||||||
self.start_fail = START_STATUS.OK
|
self.start_fail = START_STATUS.OK
|
||||||
@@ -389,6 +375,8 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t"))
|
||||||
|
|
||||||
if blade_count == nil then
|
if blade_count == nil then
|
||||||
-- no units
|
-- no units
|
||||||
log.warning("FAC: cannot start process control with 0 units assigned")
|
log.warning("FAC: cannot start process control with 0 units assigned")
|
||||||
@@ -450,7 +438,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
self.saturated = true
|
self.saturated = true
|
||||||
|
|
||||||
self.status_text = { "MONITORED MODE", "running reactors at limit" }
|
self.status_text = { "MONITORED MODE", "running reactors at limit" }
|
||||||
log.info(util.c("FAC: MAX_BURN process mode started"))
|
log.info("FAC: MAX_BURN process mode started")
|
||||||
end
|
end
|
||||||
|
|
||||||
_allocate_burn_rate(self.max_burn_combined, true)
|
_allocate_burn_rate(self.max_burn_combined, true)
|
||||||
@@ -459,7 +447,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
if state_changed then
|
if state_changed then
|
||||||
self.time_start = now
|
self.time_start = now
|
||||||
self.status_text = { "BURN RATE MODE", "running" }
|
self.status_text = { "BURN RATE MODE", "running" }
|
||||||
log.info(util.c("FAC: BURN_RATE process mode started"))
|
log.info("FAC: BURN_RATE process mode started")
|
||||||
end
|
end
|
||||||
|
|
||||||
local unallocated = _allocate_burn_rate(self.burn_target, true)
|
local unallocated = _allocate_burn_rate(self.burn_target, true)
|
||||||
@@ -473,7 +461,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
self.accumulator = 0
|
self.accumulator = 0
|
||||||
|
|
||||||
self.status_text = { "CHARGE MODE", "running control loop" }
|
self.status_text = { "CHARGE MODE", "running control loop" }
|
||||||
log.info(util.c("FAC: CHARGE mode starting PID control"))
|
log.info("FAC: CHARGE mode starting PID control")
|
||||||
elseif self.last_update ~= charge_update then
|
elseif self.last_update ~= charge_update then
|
||||||
-- convert to kFE to make constants not microscopic
|
-- convert to kFE to make constants not microscopic
|
||||||
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
|
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
|
||||||
@@ -628,7 +616,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
|
astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
|
||||||
|
|
||||||
if was_fill and not astatus.matrix_fill then
|
if was_fill and not astatus.matrix_fill then
|
||||||
log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%")
|
log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check for critical unit alarms
|
-- check for critical unit alarms
|
||||||
@@ -643,11 +631,16 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- check for facility radiation
|
-- check for facility radiation
|
||||||
if self.envd[1] ~= nil then
|
if #self.envd > 0 then
|
||||||
local envd = self.envd[1] ---@type unit_session
|
local max_rad = 0
|
||||||
local e_db = envd.get_db() ---@type envd_session_db
|
|
||||||
|
|
||||||
astatus.radiation = e_db.radiation_raw > ALARM_LIMS.FAC_HIGH_RAD
|
for i = 1, #self.envd do
|
||||||
|
local envd = self.envd[i] ---@type unit_session
|
||||||
|
local e_db = envd.get_db() ---@type envd_session_db
|
||||||
|
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
|
||||||
|
end
|
||||||
|
|
||||||
|
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
|
||||||
else
|
else
|
||||||
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
|
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
|
||||||
-- operator can restart the system or hit the stop/reset button
|
-- operator can restart the system or hit the stop/reset button
|
||||||
@@ -1093,22 +1086,22 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
build.induction = {}
|
build.induction = {}
|
||||||
for i = 1, #self.induction do
|
for i = 1, #self.induction do
|
||||||
local matrix = self.induction[i] ---@type unit_session
|
local matrix = self.induction[i] ---@type unit_session
|
||||||
build.induction[matrix.get_device_idx()] = { matrix.get_db().formed, matrix.get_db().build }
|
build.induction[i] = { matrix.get_db().formed, matrix.get_db().build }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if all or type == RTU_UNIT_TYPE.SPS then
|
if all or type == RTU_UNIT_TYPE.SPS then
|
||||||
build.sps = {}
|
build.sps = {}
|
||||||
for i = 1, #self.sps do
|
for i = 1, #self.sps do
|
||||||
local sps = self.sps[i] ---@type unit_session
|
local sps = self.sps[i] ---@type unit_session
|
||||||
build.sps[sps.get_device_idx()] = { sps.get_db().formed, sps.get_db().build }
|
build.sps[i] = { sps.get_db().formed, sps.get_db().build }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
if all or type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||||
build.tanks = {}
|
build.tanks = {}
|
||||||
for i = 1, #self.tanks do
|
for i = 1, #self.tanks do
|
||||||
local tank = self.tanks[i] ---@type unit_session
|
local tank = self.tanks[i] ---@type unit_session
|
||||||
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
|
build.tanks[tank.get_device_idx()] = { tank.get_db().formed, tank.get_db().build }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1160,7 +1153,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
for i = 1, #self.induction do
|
for i = 1, #self.induction do
|
||||||
local matrix = self.induction[i] ---@type unit_session
|
local matrix = self.induction[i] ---@type unit_session
|
||||||
local db = matrix.get_db() ---@type imatrix_session_db
|
local db = matrix.get_db() ---@type imatrix_session_db
|
||||||
status.induction[matrix.get_device_idx()] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
|
status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- status of sps
|
-- status of sps
|
||||||
@@ -1168,7 +1161,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
for i = 1, #self.sps do
|
for i = 1, #self.sps do
|
||||||
local sps = self.sps[i] ---@type unit_session
|
local sps = self.sps[i] ---@type unit_session
|
||||||
local db = sps.get_db() ---@type sps_session_db
|
local db = sps.get_db() ---@type sps_session_db
|
||||||
status.sps[sps.get_device_idx()] = { sps.is_faulted(), db.formed, db.state, db.tanks }
|
status.sps[i] = { sps.is_faulted(), db.formed, db.state, db.tanks }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- status of dynamic tanks
|
-- status of dynamic tanks
|
||||||
@@ -1180,10 +1173,11 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- radiation monitors (environment detectors)
|
-- radiation monitors (environment detectors)
|
||||||
status.rad_mon = {}
|
status.envds = {}
|
||||||
for i = 1, #self.envd do
|
for i = 1, #self.envd do
|
||||||
local envd = self.envd[i] ---@type unit_session
|
local envd = self.envd[i] ---@type unit_session
|
||||||
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
|
local db = envd.get_db() ---@type envd_session_db
|
||||||
|
status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw }
|
||||||
end
|
end
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("supervisor.config")
|
|
||||||
local databus = require("supervisor.databus")
|
local databus = require("supervisor.databus")
|
||||||
|
local supervisor = require("supervisor.supervisor")
|
||||||
|
|
||||||
local pgi = require("supervisor.panel.pgi")
|
local pgi = require("supervisor.panel.pgi")
|
||||||
local style = require("supervisor.panel.style")
|
local style = require("supervisor.panel.style")
|
||||||
@@ -88,7 +88,7 @@ local function init(panel)
|
|||||||
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||||
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
|
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
|
||||||
|
|
||||||
for i = 1, config.NUM_REACTORS do
|
for i = 1, supervisor.config.UnitCount do
|
||||||
local ps_prefix = "plc_" .. i .. "_"
|
local ps_prefix = "plc_" .. i .. "_"
|
||||||
local plc_entry = Div{parent=plc_list,height=3,fg_bg=bw_fg_bg}
|
local plc_entry = Div{parent=plc_list,height=3,fg_bg=bw_fg_bg}
|
||||||
|
|
||||||
|
|||||||
@@ -590,6 +590,9 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- check if the manual lockout for automatic control is active
|
||||||
|
function public.is_auto_locked() return self.auto_lock end
|
||||||
|
|
||||||
-- set the burn rate on behalf of automatic control
|
-- set the burn rate on behalf of automatic control
|
||||||
---@param rate number burn rate
|
---@param rate number burn rate
|
||||||
---@param ramp boolean true to ramp, false to not
|
---@param ramp boolean true to ramp, false to not
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
|||||||
-- validate unit advertisement
|
-- validate unit advertisement
|
||||||
|
|
||||||
local advert_validator = util.new_validator()
|
local advert_validator = util.new_validator()
|
||||||
advert_validator.assert_type_int(unit_advert.index)
|
advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false))
|
||||||
advert_validator.assert_type_int(unit_advert.reactor)
|
advert_validator.assert_type_int(unit_advert.reactor)
|
||||||
|
|
||||||
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
||||||
@@ -108,7 +108,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
|||||||
end
|
end
|
||||||
|
|
||||||
if advert_validator.valid() then
|
if advert_validator.valid() then
|
||||||
advert_validator.assert_min(unit_advert.index, 1)
|
if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end
|
||||||
advert_validator.assert_min(unit_advert.reactor, 0)
|
advert_validator.assert_min(unit_advert.reactor, 0)
|
||||||
advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
|
advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
|
||||||
if not advert_validator.valid() then u_type = false end
|
if not advert_validator.valid() then u_type = false end
|
||||||
|
|||||||
@@ -37,13 +37,16 @@ local PERIODICS = {
|
|||||||
---@param advert rtu_advertisement RTU advertisement table
|
---@param advert rtu_advertisement RTU advertisement table
|
||||||
---@param out_queue mqueue RTU unit message out queue
|
---@param out_queue mqueue RTU unit message out queue
|
||||||
function boilerv.new(session_id, unit_id, advert, out_queue)
|
function boilerv.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- checks
|
||||||
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
|
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
|
||||||
log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate boilerv RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
|
return nil
|
||||||
|
elseif not util.is_int(advert.index) then
|
||||||
|
log.error("attempt to instantiate boilerv RTU without index")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").boilerv(", advert.index, ")[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -49,13 +49,16 @@ local PERIODICS = {
|
|||||||
---@param advert rtu_advertisement RTU advertisement table
|
---@param advert rtu_advertisement RTU advertisement table
|
||||||
---@param out_queue mqueue RTU unit message out queue
|
---@param out_queue mqueue RTU unit message out queue
|
||||||
function dynamicv.new(session_id, unit_id, advert, out_queue)
|
function dynamicv.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- checks
|
||||||
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
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.")
|
log.error("attempt to instantiate dynamicv RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
|
return nil
|
||||||
|
elseif not util.is_int(advert.index) then
|
||||||
|
log.error("attempt to instantiate dynamicv RTU without index")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").dynamicv(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").dynamicv(", advert.index, ")[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -28,13 +28,16 @@ local PERIODICS = {
|
|||||||
---@param advert rtu_advertisement
|
---@param advert rtu_advertisement
|
||||||
---@param out_queue mqueue
|
---@param out_queue mqueue
|
||||||
function envd.new(session_id, unit_id, advert, out_queue)
|
function envd.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- checks
|
||||||
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
|
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||||
log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate envd RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
|
return nil
|
||||||
|
elseif not util.is_int(advert.index) then
|
||||||
|
log.error("attempt to instantiate envd RTU without index")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").envd(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").envd(", advert.index, ")[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ local PERIODICS = {
|
|||||||
---@param advert rtu_advertisement RTU advertisement table
|
---@param advert rtu_advertisement RTU advertisement table
|
||||||
---@param out_queue mqueue RTU unit message out queue
|
---@param out_queue mqueue RTU unit message out queue
|
||||||
function imatrix.new(session_id, unit_id, advert, out_queue)
|
function imatrix.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- checks
|
||||||
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
|
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
|
||||||
log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate imatrix RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").imatrix(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").imatrix[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -52,12 +52,11 @@ local PERIODICS = {
|
|||||||
function redstone.new(session_id, unit_id, advert, out_queue)
|
function redstone.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- type check
|
||||||
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
|
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
|
||||||
log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate redstone RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- for redstone, use unit ID not device index
|
local log_tag = util.c("session.rtu(", session_id, ").redstone[@", unit_id, "]: ")
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. unit_id .. "): "
|
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ local PERIODICS = {
|
|||||||
function sna.new(session_id, unit_id, advert, out_queue)
|
function sna.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- type check
|
||||||
if advert.type ~= RTU_UNIT_TYPE.SNA then
|
if advert.type ~= RTU_UNIT_TYPE.SNA then
|
||||||
log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate sna RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").sna(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").sna[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ local PERIODICS = {
|
|||||||
function sps.new(session_id, unit_id, advert, out_queue)
|
function sps.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- type check
|
||||||
if advert.type ~= RTU_UNIT_TYPE.SPS then
|
if advert.type ~= RTU_UNIT_TYPE.SPS then
|
||||||
log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate sps RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").sps(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").sps[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -49,13 +49,16 @@ local PERIODICS = {
|
|||||||
---@param advert rtu_advertisement RTU advertisement table
|
---@param advert rtu_advertisement RTU advertisement table
|
||||||
---@param out_queue mqueue RTU unit message out queue
|
---@param out_queue mqueue RTU unit message out queue
|
||||||
function turbinev.new(session_id, unit_id, advert, out_queue)
|
function turbinev.new(session_id, unit_id, advert, out_queue)
|
||||||
-- type check
|
-- checks
|
||||||
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
|
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||||
log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
log.error("attempt to instantiate turbinev RTU for type " .. types.rtu_type_to_string(advert.type))
|
||||||
|
return nil
|
||||||
|
elseif not util.is_int(advert.index) then
|
||||||
|
log.error("attempt to instantiate turbinev RTU without index")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local log_tag = "session.rtu(" .. session_id .. ").turbinev(" .. advert.index .. "): "
|
local log_tag = util.c("session.rtu(", session_id, ").turbinev(", advert.index, ")[@", unit_id, "]: ")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
|||||||
function public.get_unit_id() return unit_id end
|
function public.get_unit_id() return unit_id end
|
||||||
-- get the device index
|
-- get the device index
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function public.get_device_idx() return self.device_index end
|
function public.get_device_idx() return self.device_index or 0 end
|
||||||
-- get the reactor ID
|
-- get the reactor ID
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function public.get_reactor() return self.reactor end
|
function public.get_reactor() return self.reactor end
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ local log = require("scada-common.log")
|
|||||||
local mqueue = require("scada-common.mqueue")
|
local mqueue = require("scada-common.mqueue")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("supervisor.config")
|
|
||||||
local databus = require("supervisor.databus")
|
local databus = require("supervisor.databus")
|
||||||
local facility = require("supervisor.facility")
|
local facility = require("supervisor.facility")
|
||||||
|
|
||||||
local svqtypes = require("supervisor.session.svqtypes")
|
|
||||||
|
|
||||||
local coordinator = require("supervisor.session.coordinator")
|
local coordinator = require("supervisor.session.coordinator")
|
||||||
local plc = require("supervisor.session.plc")
|
local plc = require("supervisor.session.plc")
|
||||||
local pocket = require("supervisor.session.pocket")
|
local pocket = require("supervisor.session.pocket")
|
||||||
local rtu = require("supervisor.session.rtu")
|
local rtu = require("supervisor.session.rtu")
|
||||||
|
local svqtypes = require("supervisor.session.svqtypes")
|
||||||
|
|
||||||
-- Supervisor Sessions Handler
|
-- Supervisor Sessions Handler
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ svsessions.SESSION_TYPE = SESSION_TYPE
|
|||||||
local self = {
|
local self = {
|
||||||
nic = nil, ---@type nic|nil
|
nic = nil, ---@type nic|nil
|
||||||
fp_ok = false,
|
fp_ok = false,
|
||||||
num_reactors = 0,
|
config = nil, ---@type svr_config
|
||||||
facility = nil, ---@type facility|nil
|
facility = nil, ---@type facility|nil
|
||||||
sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
|
sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
|
||||||
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }
|
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 }
|
||||||
@@ -60,7 +58,7 @@ local function _sv_handle_outq(session)
|
|||||||
if msg ~= nil then
|
if msg ~= nil then
|
||||||
if msg.qtype == mqueue.TYPE.PACKET then
|
if msg.qtype == mqueue.TYPE.PACKET then
|
||||||
-- handle a packet to be sent
|
-- handle a packet to be sent
|
||||||
self.nic.transmit(session.r_chan, config.SVR_CHANNEL, msg.message)
|
self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
|
||||||
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||||
-- handle instruction/notification
|
-- handle instruction/notification
|
||||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||||
@@ -135,7 +133,7 @@ local function _shutdown(session)
|
|||||||
while session.out_queue.ready() do
|
while session.out_queue.ready() do
|
||||||
local msg = session.out_queue.pop()
|
local msg = session.out_queue.pop()
|
||||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||||
self.nic.transmit(session.r_chan, config.SVR_CHANNEL, msg.message)
|
self.nic.transmit(session.r_chan, self.config.SVR_Channel, msg.message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -197,13 +195,13 @@ end
|
|||||||
-- initialize svsessions
|
-- initialize svsessions
|
||||||
---@param nic nic network interface device
|
---@param nic nic network interface device
|
||||||
---@param fp_ok boolean front panel active
|
---@param fp_ok boolean front panel active
|
||||||
---@param num_reactors integer number of reactors
|
---@param config svr_config supervisor configuration
|
||||||
---@param cooling_conf sv_cooling_conf cooling configuration definition
|
---@param cooling_conf sv_cooling_conf cooling configuration definition
|
||||||
function svsessions.init(nic, fp_ok, num_reactors, cooling_conf)
|
function svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||||
self.nic = nic
|
self.nic = nic
|
||||||
self.fp_ok = fp_ok
|
self.fp_ok = fp_ok
|
||||||
self.num_reactors = num_reactors
|
self.config = config
|
||||||
self.facility = facility.new(num_reactors, cooling_conf)
|
self.facility = facility.new(config.UnitCount, cooling_conf)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- find an RTU session by the computer ID
|
-- find an RTU session by the computer ID
|
||||||
@@ -280,14 +278,14 @@ end
|
|||||||
---@param version string
|
---@param version string
|
||||||
---@return integer|false session_id
|
---@return integer|false session_id
|
||||||
function svsessions.establish_plc_session(source_addr, for_reactor, version)
|
function svsessions.establish_plc_session(source_addr, for_reactor, version)
|
||||||
if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.num_reactors then
|
if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.config.UnitCount then
|
||||||
---@class plc_session_struct
|
---@class plc_session_struct
|
||||||
local plc_s = {
|
local plc_s = {
|
||||||
s_type = "plc",
|
s_type = "plc",
|
||||||
open = true,
|
open = true,
|
||||||
reactor = for_reactor,
|
reactor = for_reactor,
|
||||||
version = version,
|
version = version,
|
||||||
r_chan = config.PLC_CHANNEL,
|
r_chan = self.config.PLC_Channel,
|
||||||
s_addr = source_addr,
|
s_addr = source_addr,
|
||||||
in_queue = mqueue.new(),
|
in_queue = mqueue.new(),
|
||||||
out_queue = mqueue.new(),
|
out_queue = mqueue.new(),
|
||||||
@@ -296,8 +294,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
|
|||||||
|
|
||||||
local id = self.next_ids.plc
|
local id = self.next_ids.plc
|
||||||
|
|
||||||
plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue,
|
plc_s.instance = plc.new_session(id, source_addr, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
|
||||||
config.PLC_TIMEOUT, self.fp_ok)
|
|
||||||
table.insert(self.sessions.plc, plc_s)
|
table.insert(self.sessions.plc, plc_s)
|
||||||
|
|
||||||
local units = self.facility.get_units()
|
local units = self.facility.get_units()
|
||||||
@@ -305,8 +302,7 @@ function svsessions.establish_plc_session(source_addr, for_reactor, version)
|
|||||||
|
|
||||||
local mt = {
|
local mt = {
|
||||||
---@param s plc_session_struct
|
---@param s plc_session_struct
|
||||||
__tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor,
|
__tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, " (@", s.s_addr, ")") end
|
||||||
" (@", s.s_addr, ")") end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(plc_s, mt)
|
setmetatable(plc_s, mt)
|
||||||
@@ -336,7 +332,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version)
|
|||||||
s_type = "rtu",
|
s_type = "rtu",
|
||||||
open = true,
|
open = true,
|
||||||
version = version,
|
version = version,
|
||||||
r_chan = config.RTU_CHANNEL,
|
r_chan = self.config.RTU_Channel,
|
||||||
s_addr = source_addr,
|
s_addr = source_addr,
|
||||||
in_queue = mqueue.new(),
|
in_queue = mqueue.new(),
|
||||||
out_queue = mqueue.new(),
|
out_queue = mqueue.new(),
|
||||||
@@ -345,8 +341,7 @@ function svsessions.establish_rtu_session(source_addr, advertisement, version)
|
|||||||
|
|
||||||
local id = self.next_ids.rtu
|
local id = self.next_ids.rtu
|
||||||
|
|
||||||
rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT,
|
rtu_s.instance = rtu.new_session(id, source_addr, rtu_s.in_queue, rtu_s.out_queue, self.config.RTU_Timeout, advertisement, self.facility, self.fp_ok)
|
||||||
advertisement, self.facility, self.fp_ok)
|
|
||||||
table.insert(self.sessions.rtu, rtu_s)
|
table.insert(self.sessions.rtu, rtu_s)
|
||||||
|
|
||||||
local mt = {
|
local mt = {
|
||||||
@@ -377,7 +372,7 @@ function svsessions.establish_crd_session(source_addr, version)
|
|||||||
s_type = "crd",
|
s_type = "crd",
|
||||||
open = true,
|
open = true,
|
||||||
version = version,
|
version = version,
|
||||||
r_chan = config.CRD_CHANNEL,
|
r_chan = self.config.CRD_Channel,
|
||||||
s_addr = source_addr,
|
s_addr = source_addr,
|
||||||
in_queue = mqueue.new(),
|
in_queue = mqueue.new(),
|
||||||
out_queue = mqueue.new(),
|
out_queue = mqueue.new(),
|
||||||
@@ -386,8 +381,7 @@ function svsessions.establish_crd_session(source_addr, version)
|
|||||||
|
|
||||||
local id = self.next_ids.crd
|
local id = self.next_ids.crd
|
||||||
|
|
||||||
crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, config.CRD_TIMEOUT,
|
crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, self.config.CRD_Timeout, self.facility, self.fp_ok)
|
||||||
self.facility, self.fp_ok)
|
|
||||||
table.insert(self.sessions.crd, crd_s)
|
table.insert(self.sessions.crd, crd_s)
|
||||||
|
|
||||||
local mt = {
|
local mt = {
|
||||||
@@ -421,7 +415,7 @@ function svsessions.establish_pdg_session(source_addr, version)
|
|||||||
s_type = "pkt",
|
s_type = "pkt",
|
||||||
open = true,
|
open = true,
|
||||||
version = version,
|
version = version,
|
||||||
r_chan = config.PKT_CHANNEL,
|
r_chan = self.config.PKT_Channel,
|
||||||
s_addr = source_addr,
|
s_addr = source_addr,
|
||||||
in_queue = mqueue.new(),
|
in_queue = mqueue.new(),
|
||||||
out_queue = mqueue.new(),
|
out_queue = mqueue.new(),
|
||||||
@@ -430,8 +424,7 @@ function svsessions.establish_pdg_session(source_addr, version)
|
|||||||
|
|
||||||
local id = self.next_ids.pdg
|
local id = self.next_ids.pdg
|
||||||
|
|
||||||
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.facility,
|
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok)
|
||||||
self.fp_ok)
|
|
||||||
table.insert(self.sessions.pdg, pdg_s)
|
table.insert(self.sessions.pdg, pdg_s)
|
||||||
|
|
||||||
local mt = {
|
local mt = {
|
||||||
|
|||||||
@@ -14,70 +14,67 @@ local util = require("scada-common.util")
|
|||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local config = require("supervisor.config")
|
local configure = require("supervisor.configure")
|
||||||
local databus = require("supervisor.databus")
|
local databus = require("supervisor.databus")
|
||||||
local renderer = require("supervisor.renderer")
|
local renderer = require("supervisor.renderer")
|
||||||
local supervisor = require("supervisor.supervisor")
|
local supervisor = require("supervisor.supervisor")
|
||||||
|
|
||||||
local svsessions = require("supervisor.session.svsessions")
|
local svsessions = require("supervisor.session.svsessions")
|
||||||
|
|
||||||
local SUPERVISOR_VERSION = "v1.0.8"
|
local SUPERVISOR_VERSION = "v1.2.8"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- config validation
|
-- get configuration
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
|
if not supervisor.load_config() then
|
||||||
|
-- try to reconfigure (user action)
|
||||||
|
local success, error = configure.configure(true)
|
||||||
|
if success then
|
||||||
|
assert(supervisor.load_config(), "failed to load valid configuration")
|
||||||
|
else
|
||||||
|
assert(success, "supervisor configuration error: " .. error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local config = supervisor.config
|
||||||
|
|
||||||
local cfv = util.new_validator()
|
local cfv = util.new_validator()
|
||||||
|
|
||||||
cfv.assert_channel(config.SVR_CHANNEL)
|
cfv.assert_eq(#config.CoolingConfig, config.UnitCount)
|
||||||
cfv.assert_channel(config.PLC_CHANNEL)
|
assert(cfv.valid(), "startup> the number of reactor cooling configurations is different than the number of units")
|
||||||
cfv.assert_channel(config.RTU_CHANNEL)
|
|
||||||
cfv.assert_channel(config.CRD_CHANNEL)
|
|
||||||
cfv.assert_channel(config.PKT_CHANNEL)
|
|
||||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
|
||||||
cfv.assert_type_num(config.PLC_TIMEOUT)
|
|
||||||
cfv.assert_min(config.PLC_TIMEOUT, 2)
|
|
||||||
cfv.assert_type_num(config.RTU_TIMEOUT)
|
|
||||||
cfv.assert_min(config.RTU_TIMEOUT, 2)
|
|
||||||
cfv.assert_type_num(config.CRD_TIMEOUT)
|
|
||||||
cfv.assert_min(config.CRD_TIMEOUT, 2)
|
|
||||||
cfv.assert_type_num(config.PKT_TIMEOUT)
|
|
||||||
cfv.assert_min(config.PKT_TIMEOUT, 2)
|
|
||||||
cfv.assert_type_int(config.NUM_REACTORS)
|
|
||||||
cfv.assert_type_table(config.REACTOR_COOLING)
|
|
||||||
cfv.assert_type_int(config.FAC_TANK_MODE)
|
|
||||||
cfv.assert_type_table(config.FAC_TANK_DEFS)
|
|
||||||
cfv.assert_type_str(config.LOG_PATH)
|
|
||||||
cfv.assert_type_int(config.LOG_MODE)
|
|
||||||
|
|
||||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
for i = 1, config.UnitCount do
|
||||||
|
cfv.assert_type_table(config.CoolingConfig[i])
|
||||||
|
assert(cfv.valid(), "startup> missing cooling entry for reactor unit " .. i)
|
||||||
|
cfv.assert_type_int(config.CoolingConfig[i].BoilerCount)
|
||||||
|
cfv.assert_type_int(config.CoolingConfig[i].TurbineCount)
|
||||||
|
cfv.assert_type_bool(config.CoolingConfig[i].TankConnection)
|
||||||
|
assert(cfv.valid(), "startup> missing boiler/turbine/tank fields for reactor unit " .. i)
|
||||||
|
cfv.assert_range(config.CoolingConfig[i].BoilerCount, 0, 2)
|
||||||
|
cfv.assert_range(config.CoolingConfig[i].TurbineCount, 1, 3)
|
||||||
|
assert(cfv.valid(), "startup> out-of-range number of boilers and/or turbines provided for reactor unit " .. i)
|
||||||
|
end
|
||||||
|
|
||||||
assert((config.FAC_TANK_MODE == 0) or (config.NUM_REACTORS == #config.FAC_TANK_DEFS),
|
if config.FacilityTankMode > 0 then
|
||||||
"bad config file: FAC_TANK_DEFS length not equal to NUM_REACTORS")
|
assert(config.UnitCount == #config.FacilityTankDefs, "startup> the number of facility tank definitions must be equal to the number of units in facility tank mode")
|
||||||
|
|
||||||
cfv.assert_eq(#config.REACTOR_COOLING, config.NUM_REACTORS)
|
for i = 1, config.UnitCount do
|
||||||
assert(cfv.valid(), "config: number of cooling configs different than number of units")
|
local def = config.FacilityTankDefs[i]
|
||||||
|
cfv.assert_type_int(def)
|
||||||
for i = 1, config.NUM_REACTORS do
|
cfv.assert_range(def, 0, 2)
|
||||||
cfv.assert_type_table(config.REACTOR_COOLING[i])
|
assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i)
|
||||||
assert(cfv.valid(), "config: missing cooling entry for reactor " .. i)
|
end
|
||||||
cfv.assert_type_int(config.REACTOR_COOLING[i].BOILERS)
|
|
||||||
cfv.assert_type_int(config.REACTOR_COOLING[i].TURBINES)
|
|
||||||
cfv.assert_type_bool(config.REACTOR_COOLING[i].TANK)
|
|
||||||
assert(cfv.valid(), "config: missing boilers/turbines for reactor " .. i)
|
|
||||||
cfv.assert_min(config.REACTOR_COOLING[i].BOILERS, 0)
|
|
||||||
cfv.assert_min(config.REACTOR_COOLING[i].TURBINES, 1)
|
|
||||||
assert(cfv.valid(), "config: bad number of boilers/turbines for reactor " .. i)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- log init
|
-- log init
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||||
|
|
||||||
log.info("========================================")
|
log.info("========================================")
|
||||||
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
|
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
|
||||||
@@ -102,8 +99,8 @@ local function main()
|
|||||||
ppm.mount_all()
|
ppm.mount_all()
|
||||||
|
|
||||||
-- message authentication init
|
-- message authentication init
|
||||||
if type(config.AUTH_KEY) == "string" then
|
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||||
network.init_mac(config.AUTH_KEY)
|
network.init_mac(config.AuthKey)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- get modem
|
-- get modem
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ local comms = require("scada-common.comms")
|
|||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local config = require("supervisor.config")
|
|
||||||
|
|
||||||
local svsessions = require("supervisor.session.svsessions")
|
local svsessions = require("supervisor.session.svsessions")
|
||||||
|
|
||||||
local supervisor = {}
|
local supervisor = {}
|
||||||
@@ -13,6 +11,79 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
|
|||||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||||
local MGMT_TYPE = comms.MGMT_TYPE
|
local MGMT_TYPE = comms.MGMT_TYPE
|
||||||
|
|
||||||
|
---@type svr_config
|
||||||
|
local config = {}
|
||||||
|
|
||||||
|
supervisor.config = config
|
||||||
|
|
||||||
|
-- load the supervisor configuration
|
||||||
|
function supervisor.load_config()
|
||||||
|
if not settings.load("/supervisor.settings") then return false end
|
||||||
|
|
||||||
|
config.UnitCount = settings.get("UnitCount")
|
||||||
|
config.CoolingConfig = settings.get("CoolingConfig")
|
||||||
|
config.FacilityTankMode = settings.get("FacilityTankMode")
|
||||||
|
config.FacilityTankDefs = settings.get("FacilityTankDefs")
|
||||||
|
|
||||||
|
config.SVR_Channel = settings.get("SVR_Channel")
|
||||||
|
config.PLC_Channel = settings.get("PLC_Channel")
|
||||||
|
config.RTU_Channel = settings.get("RTU_Channel")
|
||||||
|
config.CRD_Channel = settings.get("CRD_Channel")
|
||||||
|
config.PKT_Channel = settings.get("PKT_Channel")
|
||||||
|
|
||||||
|
config.PLC_Timeout = settings.get("PLC_Timeout")
|
||||||
|
config.RTU_Timeout = settings.get("RTU_Timeout")
|
||||||
|
config.CRD_Timeout = settings.get("CRD_Timeout")
|
||||||
|
config.PKT_Timeout = settings.get("PKT_Timeout")
|
||||||
|
|
||||||
|
config.TrustedRange = settings.get("TrustedRange")
|
||||||
|
config.AuthKey = settings.get("AuthKey")
|
||||||
|
|
||||||
|
config.LogMode = settings.get("LogMode")
|
||||||
|
config.LogPath = settings.get("LogPath")
|
||||||
|
config.LogDebug = settings.get("LogDebug")
|
||||||
|
|
||||||
|
local cfv = util.new_validator()
|
||||||
|
|
||||||
|
cfv.assert_type_int(config.UnitCount)
|
||||||
|
cfv.assert_range(config.UnitCount, 1, 4)
|
||||||
|
|
||||||
|
cfv.assert_type_table(config.CoolingConfig)
|
||||||
|
cfv.assert_type_table(config.FacilityTankDefs)
|
||||||
|
cfv.assert_type_int(config.FacilityTankMode)
|
||||||
|
cfv.assert_range(config.FacilityTankMode, 0, 8)
|
||||||
|
|
||||||
|
cfv.assert_channel(config.SVR_Channel)
|
||||||
|
cfv.assert_channel(config.PLC_Channel)
|
||||||
|
cfv.assert_channel(config.RTU_Channel)
|
||||||
|
cfv.assert_channel(config.CRD_Channel)
|
||||||
|
cfv.assert_channel(config.PKT_Channel)
|
||||||
|
|
||||||
|
cfv.assert_type_num(config.PLC_Timeout)
|
||||||
|
cfv.assert_min(config.PLC_Timeout, 2)
|
||||||
|
cfv.assert_type_num(config.RTU_Timeout)
|
||||||
|
cfv.assert_min(config.RTU_Timeout, 2)
|
||||||
|
cfv.assert_type_num(config.CRD_Timeout)
|
||||||
|
cfv.assert_min(config.CRD_Timeout, 2)
|
||||||
|
cfv.assert_type_num(config.PKT_Timeout)
|
||||||
|
cfv.assert_min(config.PKT_Timeout, 2)
|
||||||
|
|
||||||
|
cfv.assert_type_num(config.TrustedRange)
|
||||||
|
cfv.assert_min(config.TrustedRange, 0)
|
||||||
|
|
||||||
|
if type(config.AuthKey) == "string" then
|
||||||
|
local len = string.len(config.AuthKey)
|
||||||
|
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
cfv.assert_type_int(config.LogMode)
|
||||||
|
cfv.assert_range(config.LogMode, 0, 1)
|
||||||
|
cfv.assert_type_str(config.LogPath)
|
||||||
|
cfv.assert_type_bool(config.LogDebug)
|
||||||
|
|
||||||
|
return cfv.valid()
|
||||||
|
end
|
||||||
|
|
||||||
-- supervisory controller communications
|
-- supervisory controller communications
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param _version string supervisor version
|
---@param _version string supervisor version
|
||||||
@@ -23,32 +94,23 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
-- print a log message to the terminal as long as the UI isn't running
|
-- print a log message to the terminal as long as the UI isn't running
|
||||||
local function println(message) if not fp_ok then util.println_ts(message) end end
|
local function println(message) if not fp_ok then util.println_ts(message) end end
|
||||||
|
|
||||||
-- channel list from config
|
|
||||||
local svr_channel = config.SVR_CHANNEL
|
|
||||||
local plc_channel = config.PLC_CHANNEL
|
|
||||||
local rtu_channel = config.RTU_CHANNEL
|
|
||||||
local crd_channel = config.CRD_CHANNEL
|
|
||||||
local pkt_channel = config.PKT_CHANNEL
|
|
||||||
|
|
||||||
-- configuration data
|
|
||||||
local num_reactors = config.NUM_REACTORS
|
|
||||||
---@class sv_cooling_conf
|
---@class sv_cooling_conf
|
||||||
local cooling_conf = { r_cool = config.REACTOR_COOLING, fac_tank_mode = config.FAC_TANK_MODE, fac_tank_defs = config.FAC_TANK_DEFS }
|
local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs }
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
last_est_acks = {}
|
last_est_acks = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
comms.set_trusted_range(config.TRUSTED_RANGE)
|
comms.set_trusted_range(config.TrustedRange)
|
||||||
|
|
||||||
-- PRIVATE FUNCTIONS --
|
-- PRIVATE FUNCTIONS --
|
||||||
|
|
||||||
-- configure modem channels
|
-- configure modem channels
|
||||||
nic.closeAll()
|
nic.closeAll()
|
||||||
nic.open(svr_channel)
|
nic.open(config.SVR_Channel)
|
||||||
|
|
||||||
-- pass modem, status, and config data to svsessions
|
-- pass modem, status, and config data to svsessions
|
||||||
svsessions.init(nic, fp_ok, num_reactors, cooling_conf)
|
svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||||
|
|
||||||
-- send an establish request response
|
-- send an establish request response
|
||||||
---@param packet scada_packet
|
---@param packet scada_packet
|
||||||
@@ -61,7 +123,7 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
||||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||||
|
|
||||||
nic.transmit(packet.remote_channel(), svr_channel, s_pkt)
|
nic.transmit(packet.remote_channel(), config.SVR_Channel, s_pkt)
|
||||||
self.last_est_acks[packet.src_addr()] = ack
|
self.last_est_acks[packet.src_addr()] = ack
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -124,9 +186,9 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
local src_addr = packet.scada_frame.src_addr()
|
local src_addr = packet.scada_frame.src_addr()
|
||||||
local protocol = packet.scada_frame.protocol()
|
local protocol = packet.scada_frame.protocol()
|
||||||
|
|
||||||
if l_chan ~= svr_channel then
|
if l_chan ~= config.SVR_Channel then
|
||||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||||
elseif r_chan == plc_channel then
|
elseif r_chan == config.PLC_Channel then
|
||||||
-- look for an associated session
|
-- look for an associated session
|
||||||
local session = svsessions.find_plc_session(src_addr)
|
local session = svsessions.find_plc_session(src_addr)
|
||||||
|
|
||||||
@@ -137,9 +199,8 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
-- pass the packet onto the session handler
|
-- pass the packet onto the session handler
|
||||||
session.in_queue.push_packet(packet)
|
session.in_queue.push_packet(packet)
|
||||||
else
|
else
|
||||||
-- unknown session, force a re-link
|
-- any other packet should be session related, discard it
|
||||||
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink")
|
log.debug("discarding RPLC packet without a known session")
|
||||||
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
|
|
||||||
end
|
end
|
||||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||||
---@cast packet mgmt_frame
|
---@cast packet mgmt_frame
|
||||||
@@ -201,7 +262,7 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
else
|
else
|
||||||
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
|
log.debug(util.c("illegal packet type ", protocol, " on PLC channel"))
|
||||||
end
|
end
|
||||||
elseif r_chan == rtu_channel then
|
elseif r_chan == config.RTU_Channel then
|
||||||
-- look for an associated session
|
-- look for an associated session
|
||||||
local session = svsessions.find_rtu_session(src_addr)
|
local session = svsessions.find_rtu_session(src_addr)
|
||||||
|
|
||||||
@@ -265,7 +326,7 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
else
|
else
|
||||||
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
|
log.debug(util.c("illegal packet type ", protocol, " on RTU channel"))
|
||||||
end
|
end
|
||||||
elseif r_chan == crd_channel then
|
elseif r_chan == config.CRD_Channel then
|
||||||
-- look for an associated session
|
-- look for an associated session
|
||||||
local session = svsessions.find_crd_session(src_addr)
|
local session = svsessions.find_crd_session(src_addr)
|
||||||
|
|
||||||
@@ -299,7 +360,7 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
|
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
|
||||||
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
|
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
|
||||||
|
|
||||||
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { num_reactors, cooling_conf })
|
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf })
|
||||||
else
|
else
|
||||||
if last_ack ~= ESTABLISH_ACK.COLLISION then
|
if last_ack ~= ESTABLISH_ACK.COLLISION then
|
||||||
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
|
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
|
||||||
@@ -332,7 +393,7 @@ function supervisor.comms(_version, nic, fp_ok)
|
|||||||
else
|
else
|
||||||
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
|
log.debug(util.c("illegal packet type ", protocol, " on coordinator channel"))
|
||||||
end
|
end
|
||||||
elseif r_chan == pkt_channel then
|
elseif r_chan == config.PKT_Channel then
|
||||||
-- look for an associated session
|
-- look for an associated session
|
||||||
local session = svsessions.find_pdg_session(src_addr)
|
local session = svsessions.find_pdg_session(src_addr)
|
||||||
|
|
||||||
|
|||||||
@@ -498,7 +498,6 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
self.plc_s = nil
|
self.plc_s = nil
|
||||||
self.plc_i = nil
|
self.plc_i = nil
|
||||||
self.db.control.br100 = 0
|
self.db.control.br100 = 0
|
||||||
self.db.control.lim_br100 = 0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- unlink RTU unit sessions if they are closed
|
-- unlink RTU unit sessions if they are closed
|
||||||
@@ -525,12 +524,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check plc formed/faulted
|
-- plc instance checks
|
||||||
if self.plc_i ~= nil then
|
if self.plc_i ~= nil then
|
||||||
|
-- check if degraded
|
||||||
local rps = self.plc_i.get_rps()
|
local rps = self.plc_i.get_rps()
|
||||||
if rps.fault or rps.sys_fail then
|
if rps.fault or rps.sys_fail then self.db.control.degraded = true end
|
||||||
self.db.control.degraded = true
|
|
||||||
end
|
-- re-engage auto lock if it reconnected without it
|
||||||
|
if self.auto_engaged and not self.plc_i.is_auto_locked() then self.plc_i.auto_lock(true) end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- update deltas
|
-- update deltas
|
||||||
@@ -563,6 +564,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
function public.auto_engage()
|
function public.auto_engage()
|
||||||
self.auto_engaged = true
|
self.auto_engaged = true
|
||||||
if self.plc_i ~= nil then
|
if self.plc_i ~= nil then
|
||||||
|
log.debug(util.c("UNIT ", self.r_id, ": engaged auto control"))
|
||||||
self.plc_i.auto_lock(true)
|
self.plc_i.auto_lock(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -571,6 +573,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
function public.auto_disengage()
|
function public.auto_disengage()
|
||||||
self.auto_engaged = false
|
self.auto_engaged = false
|
||||||
if self.plc_i ~= nil then
|
if self.plc_i ~= nil then
|
||||||
|
log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control"))
|
||||||
self.plc_i.auto_lock(false)
|
self.plc_i.auto_lock(false)
|
||||||
self.db.control.br100 = 0
|
self.db.control.br100 = 0
|
||||||
end
|
end
|
||||||
@@ -581,12 +584,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@return integer lim_br100
|
---@return integer lim_br100
|
||||||
function public.auto_get_effective_limit()
|
function public.auto_get_effective_limit()
|
||||||
if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then
|
local ctrl = self.db.control
|
||||||
self.db.control.br100 = 0
|
if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then
|
||||||
|
-- log.debug(util.c("UNIT ", self.r_id, ": effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]"))
|
||||||
|
ctrl.br100 = 0
|
||||||
return 0
|
return 0
|
||||||
else
|
else return ctrl.lim_br100 end
|
||||||
return self.db.control.lim_br100
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set the automatic burn rate based on the last set burn rate in 100ths
|
-- set the automatic burn rate based on the last set burn rate in 100ths
|
||||||
@@ -594,8 +597,8 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
function public.auto_commit_br100(ramp)
|
function public.auto_commit_br100(ramp)
|
||||||
if self.auto_engaged then
|
if self.auto_engaged then
|
||||||
if self.plc_i ~= nil then
|
if self.plc_i ~= nil then
|
||||||
|
log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp))
|
||||||
self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
|
self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
|
||||||
|
|
||||||
if ramp then self.ramp_target_br100 = self.db.control.br100 end
|
if ramp then self.ramp_target_br100 = self.db.control.br100 end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -863,10 +866,11 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
status.sna = { #self.snas, public.get_sna_rate(), total_peak }
|
status.sna = { #self.snas, public.get_sna_rate(), total_peak }
|
||||||
|
|
||||||
-- radiation monitors (environment detectors)
|
-- radiation monitors (environment detectors)
|
||||||
status.rad_mon = {}
|
status.envds = {}
|
||||||
for i = 1, #self.envd do
|
for i = 1, #self.envd do
|
||||||
local envd = self.envd[i] ---@type unit_session
|
local envd = self.envd[i] ---@type unit_session
|
||||||
status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
|
local db = envd.get_db() ---@type envd_session_db
|
||||||
|
status.envds[envd.get_device_idx()] = { envd.is_faulted(), db.radiation, db.radiation_raw }
|
||||||
end
|
end
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ function logic.update_annunciator(self)
|
|||||||
|
|
||||||
local num_boilers = self.num_boilers
|
local num_boilers = self.num_boilers
|
||||||
local num_turbines = self.num_turbines
|
local num_turbines = self.num_turbines
|
||||||
|
local annunc = self.db.annunciator
|
||||||
|
|
||||||
self.db.annunciator.RCSFault = false
|
annunc.RCSFault = false
|
||||||
|
|
||||||
-- variables for boiler, or reactor if no boilers used
|
-- variables for boiler, or reactor if no boilers used
|
||||||
local total_boil_rate = 0.0
|
local total_boil_rate = 0.0
|
||||||
@@ -57,14 +58,14 @@ function logic.update_annunciator(self)
|
|||||||
-- REACTOR --
|
-- REACTOR --
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
self.db.annunciator.AutoControl = self.auto_engaged
|
annunc.AutoControl = self.auto_engaged
|
||||||
|
|
||||||
-- check PLC status
|
-- check PLC status
|
||||||
self.db.annunciator.PLCOnline = self.plc_i ~= nil
|
annunc.PLCOnline = self.plc_i ~= nil
|
||||||
|
|
||||||
local plc_ready = self.db.annunciator.PLCOnline
|
local plc_ready = annunc.PLCOnline
|
||||||
|
|
||||||
if self.db.annunciator.PLCOnline then
|
if plc_ready then
|
||||||
local plc_db = self.plc_i.get_db()
|
local plc_db = self.plc_i.get_db()
|
||||||
|
|
||||||
-- update ready state
|
-- update ready state
|
||||||
@@ -75,7 +76,7 @@ function logic.update_annunciator(self)
|
|||||||
(next(self.plc_i.get_status()) ~= nil) and (next(self.plc_i.get_struct()) ~= nil)
|
(next(self.plc_i.get_status()) ~= nil) and (next(self.plc_i.get_struct()) ~= nil)
|
||||||
|
|
||||||
-- update auto control limit
|
-- update auto control limit
|
||||||
if (self.db.control.lim_br100 == 0) or ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then
|
if (plc_db.mek_struct.max_burn > 0) and ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then
|
||||||
self.db.control.lim_br100 = math.floor(plc_db.mek_struct.max_burn * 100)
|
self.db.control.lim_br100 = math.floor(plc_db.mek_struct.max_burn * 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -110,29 +111,29 @@ function logic.update_annunciator(self)
|
|||||||
|
|
||||||
-- heartbeat blink about every second
|
-- heartbeat blink about every second
|
||||||
if self.last_heartbeat + 1000 < plc_db.last_status_update then
|
if self.last_heartbeat + 1000 < plc_db.last_status_update then
|
||||||
self.db.annunciator.PLCHeartbeat = not self.db.annunciator.PLCHeartbeat
|
annunc.PLCHeartbeat = not annunc.PLCHeartbeat
|
||||||
self.last_heartbeat = plc_db.last_status_update
|
self.last_heartbeat = plc_db.last_status_update
|
||||||
end
|
end
|
||||||
|
|
||||||
local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O)
|
local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O)
|
||||||
|
|
||||||
-- update other annunciator fields
|
-- update other annunciator fields
|
||||||
self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped
|
annunc.ReactorSCRAM = plc_db.rps_tripped
|
||||||
self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
|
annunc.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
|
||||||
self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC
|
annunc.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC
|
||||||
self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool)
|
annunc.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool)
|
||||||
self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low
|
annunc.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low
|
||||||
self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
|
annunc.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
|
||||||
self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
|
annunc.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
|
||||||
self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
|
annunc.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
|
||||||
self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
|
annunc.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
|
||||||
self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
|
annunc.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
|
||||||
|
|
||||||
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
|
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
|
||||||
local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv)
|
local high_rate = plc_db.mek_status.burn_rate >= (plc_db.mek_status.ccool_amnt * 0.27 / heating_rate_conv)
|
||||||
-- this advisory applies when no coolant is buffered (which we can't easily determine)<br>
|
-- this advisory applies when no coolant is buffered (which we can't easily determine)<br>
|
||||||
-- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation
|
-- it's a rough estimation, see GitHub cc-mek-scada/wiki/High-Rate-Calculation
|
||||||
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate
|
annunc.HighStartupRate = not plc_db.mek_status.status and high_rate
|
||||||
|
|
||||||
-- if no boilers, use reactor heating rate to check for boil rate mismatch
|
-- if no boilers, use reactor heating rate to check for boil rate mismatch
|
||||||
if num_boilers == 0 then
|
if num_boilers == 0 then
|
||||||
@@ -146,21 +147,25 @@ function logic.update_annunciator(self)
|
|||||||
-- MISC RTUs --
|
-- MISC RTUs --
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
self.db.annunciator.RadiationMonitor = 1
|
local max_rad, any_faulted = 0, false
|
||||||
self.db.annunciator.RadiationWarning = false
|
|
||||||
for i = 1, #self.envd do
|
for i = 1, #self.envd do
|
||||||
local envd = self.envd[i] ---@type unit_session
|
local envd = self.envd[i] ---@type unit_session
|
||||||
self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3)
|
local db = envd.get_db() ---@type envd_session_db
|
||||||
self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw >= ANNUNC_LIMS.RadiationWarning
|
any_faulted = any_faulted or envd.is_faulted()
|
||||||
break
|
if db.radiation_raw > max_rad then max_rad = db.radiation_raw end
|
||||||
end
|
end
|
||||||
|
|
||||||
self.db.annunciator.EmergencyCoolant = 1
|
annunc.RadiationMonitor = util.trinary(#self.envd == 0, 1, util.trinary(any_faulted, 2, 3))
|
||||||
|
annunc.RadiationWarning = max_rad >= ANNUNC_LIMS.RadiationWarning
|
||||||
|
|
||||||
|
annunc.EmergencyCoolant = 1
|
||||||
|
|
||||||
for i = 1, #self.redstone do
|
for i = 1, #self.redstone do
|
||||||
local db = self.redstone[i].get_db() ---@type redstone_session_db
|
local db = self.redstone[i].get_db() ---@type redstone_session_db
|
||||||
local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil
|
local io = db.io[IO.U_EMER_COOL] ---@type rs_db_dig_io|nil
|
||||||
if io ~= nil then
|
if io ~= nil then
|
||||||
self.db.annunciator.EmergencyCoolant = util.trinary(io.read(), 3, 2)
|
annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -172,7 +177,7 @@ function logic.update_annunciator(self)
|
|||||||
local boilers_ready = num_boilers == #self.boilers
|
local boilers_ready = num_boilers == #self.boilers
|
||||||
|
|
||||||
-- clear boiler online flags
|
-- clear boiler online flags
|
||||||
for i = 1, num_boilers do self.db.annunciator.BoilerOnline[i] = false end
|
for i = 1, num_boilers do annunc.BoilerOnline[i] = false end
|
||||||
|
|
||||||
-- aggregated statistics
|
-- aggregated statistics
|
||||||
local boiler_steam_dt_sum = 0.0
|
local boiler_steam_dt_sum = 0.0
|
||||||
@@ -185,7 +190,7 @@ function logic.update_annunciator(self)
|
|||||||
local boiler = session.get_db() ---@type boilerv_session_db
|
local boiler = session.get_db() ---@type boilerv_session_db
|
||||||
local idx = session.get_device_idx()
|
local idx = session.get_device_idx()
|
||||||
|
|
||||||
self.db.annunciator.RCSFault = self.db.annunciator.RCSFault or (not boiler.formed) or session.is_faulted()
|
annunc.RCSFault = annunc.RCSFault or (not boiler.formed) or session.is_faulted()
|
||||||
|
|
||||||
-- update ready state
|
-- update ready state
|
||||||
-- - must be formed
|
-- - must be formed
|
||||||
@@ -199,8 +204,8 @@ function logic.update_annunciator(self)
|
|||||||
boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. idx)
|
boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. idx)
|
||||||
boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. idx)
|
boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. idx)
|
||||||
|
|
||||||
self.db.annunciator.BoilerOnline[idx] = true
|
annunc.BoilerOnline[idx] = true
|
||||||
self.db.annunciator.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow
|
annunc.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check heating rate low
|
-- check heating rate low
|
||||||
@@ -209,14 +214,14 @@ function logic.update_annunciator(self)
|
|||||||
|
|
||||||
-- check for inactive boilers while reactor is active
|
-- check for inactive boilers while reactor is active
|
||||||
for i = 1, #self.boilers do
|
for i = 1, #self.boilers do
|
||||||
local boiler = self.boilers[i] ---@type unit_session
|
local boiler = self.boilers[i] ---@type unit_session
|
||||||
local idx = boiler.get_device_idx()
|
local idx = boiler.get_device_idx()
|
||||||
local db = boiler.get_db() ---@type boilerv_session_db
|
local db = boiler.get_db() ---@type boilerv_session_db
|
||||||
|
|
||||||
if r_db.mek_status.status then
|
if r_db.mek_status.status then
|
||||||
self.db.annunciator.HeatingRateLow[idx] = db.state.boil_rate == 0
|
annunc.HeatingRateLow[idx] = db.state.boil_rate == 0
|
||||||
else
|
else
|
||||||
self.db.annunciator.HeatingRateLow[idx] = false
|
annunc.HeatingRateLow[idx] = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -234,9 +239,9 @@ function logic.update_annunciator(self)
|
|||||||
|
|
||||||
if num_boilers > 0 then
|
if num_boilers > 0 then
|
||||||
for i = 1, #self.boilers do
|
for i = 1, #self.boilers do
|
||||||
local boiler = self.boilers[i] ---@type unit_session
|
local boiler = self.boilers[i] ---@type unit_session
|
||||||
local idx = boiler.get_device_idx()
|
local idx = boiler.get_device_idx()
|
||||||
local db = boiler.get_db() ---@type boilerv_session_db
|
local db = boiler.get_db() ---@type boilerv_session_db
|
||||||
|
|
||||||
local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1
|
local gaining_hc = _get_dt(DT_KEYS.BoilerHCool .. idx) > 10.0 or db.tanks.hcool_fill == 1
|
||||||
|
|
||||||
@@ -256,7 +261,7 @@ function logic.update_annunciator(self)
|
|||||||
cfmismatch = cfmismatch or _get_dt(DT_KEYS.ReactorCCool) < -10.0 or (gaining_hc and r_db.mek_status.ccool_fill == 0)
|
cfmismatch = cfmismatch or _get_dt(DT_KEYS.ReactorCCool) < -10.0 or (gaining_hc and r_db.mek_status.ccool_fill == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
self.db.annunciator.CoolantFeedMismatch = cfmismatch
|
annunc.CoolantFeedMismatch = cfmismatch
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
-- TURBINES --
|
-- TURBINES --
|
||||||
@@ -265,7 +270,7 @@ function logic.update_annunciator(self)
|
|||||||
local turbines_ready = num_turbines == #self.turbines
|
local turbines_ready = num_turbines == #self.turbines
|
||||||
|
|
||||||
-- clear turbine online flags
|
-- clear turbine online flags
|
||||||
for i = 1, num_turbines do self.db.annunciator.TurbineOnline[i] = false end
|
for i = 1, num_turbines do annunc.TurbineOnline[i] = false end
|
||||||
|
|
||||||
-- aggregated statistics
|
-- aggregated statistics
|
||||||
local total_flow_rate = 0
|
local total_flow_rate = 0
|
||||||
@@ -277,10 +282,10 @@ function logic.update_annunciator(self)
|
|||||||
|
|
||||||
-- go through turbines for stats and online
|
-- go through turbines for stats and online
|
||||||
for i = 1, #self.turbines do
|
for i = 1, #self.turbines do
|
||||||
local session = self.turbines[i] ---@type unit_session
|
local session = self.turbines[i] ---@type unit_session
|
||||||
local turbine = session.get_db() ---@type turbinev_session_db
|
local turbine = session.get_db() ---@type turbinev_session_db
|
||||||
|
|
||||||
self.db.annunciator.RCSFault = self.db.annunciator.RCSFault or (not turbine.formed) or session.is_faulted()
|
annunc.RCSFault = annunc.RCSFault or (not turbine.formed) or session.is_faulted()
|
||||||
|
|
||||||
-- update ready state
|
-- update ready state
|
||||||
-- - must be formed
|
-- - must be formed
|
||||||
@@ -295,59 +300,44 @@ function logic.update_annunciator(self)
|
|||||||
max_water_return_rate = max_water_return_rate + turbine.build.max_water_output
|
max_water_return_rate = max_water_return_rate + turbine.build.max_water_output
|
||||||
self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades
|
self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades
|
||||||
|
|
||||||
self.db.annunciator.TurbineOnline[session.get_device_idx()] = true
|
annunc.TurbineOnline[session.get_device_idx()] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine
|
-- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine
|
||||||
self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
|
annunc.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
|
||||||
|
|
||||||
-- check for steam feed mismatch and max return rate
|
-- check for steam feed mismatch and max return rate
|
||||||
local steam_dt_max = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MaxSteamDT_H20, ANNUNC_LIMS.SFM_MaxSteamDT_NA)
|
local steam_dt_max = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MaxSteamDT_H20, ANNUNC_LIMS.SFM_MaxSteamDT_NA)
|
||||||
local water_dt_min = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MinWaterDT_H20, ANNUNC_LIMS.SFM_MinWaterDT_NA)
|
local water_dt_min = util.trinary(num_boilers == 0, ANNUNC_LIMS.SFM_MinWaterDT_H20, ANNUNC_LIMS.SFM_MinWaterDT_NA)
|
||||||
local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch
|
local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch
|
||||||
sfmismatch = sfmismatch or boiler_steam_dt_sum > steam_dt_max or boiler_water_dt_sum < water_dt_min
|
sfmismatch = sfmismatch or boiler_steam_dt_sum > steam_dt_max or boiler_water_dt_sum < water_dt_min
|
||||||
self.db.annunciator.SteamFeedMismatch = sfmismatch
|
annunc.SteamFeedMismatch = sfmismatch
|
||||||
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
|
annunc.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
|
||||||
|
|
||||||
-- turbine safety checks
|
-- turbine safety checks
|
||||||
for i = 1, #self.turbines do
|
for i = 1, #self.turbines do
|
||||||
local turbine = self.turbines[i] ---@type unit_session
|
local turbine = self.turbines[i] ---@type unit_session
|
||||||
local db = turbine.get_db() ---@type turbinev_session_db
|
local db = turbine.get_db() ---@type turbinev_session_db
|
||||||
local idx = turbine.get_device_idx()
|
local idx = turbine.get_device_idx()
|
||||||
|
|
||||||
-- check if steam dumps are open
|
-- check if steam dumps are open
|
||||||
if db.state.dumping_mode == DUMPING_MODE.IDLE then
|
if db.state.dumping_mode == DUMPING_MODE.IDLE then
|
||||||
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK
|
annunc.SteamDumpOpen[idx] = TRI_FAIL.OK
|
||||||
elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then
|
elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then
|
||||||
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL
|
annunc.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL
|
||||||
else
|
else
|
||||||
self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL
|
annunc.SteamDumpOpen[idx] = TRI_FAIL.FULL
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check if turbines are at max speed but not keeping up
|
-- check if turbines are at max speed but not keeping up
|
||||||
self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0)
|
annunc.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0.0)
|
||||||
|
|
||||||
--[[
|
-- see notes at cc-mek-scada/wiki/Annunciator-Panels#Generator-Trip
|
||||||
Generator Trip
|
annunc.GeneratorTrip[idx] = (_get_dt(DT_KEYS.TurbinePower .. idx) > 0.0) and (db.tanks.energy_fill > 0.05)
|
||||||
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) and (db.tanks.energy_fill > 0.05)
|
|
||||||
|
|
||||||
--[[
|
-- see notes at cc-mek-scada/wiki/Annunciator-Panels#Turbine-Trip
|
||||||
Turbine Trip
|
|
||||||
a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool.
|
|
||||||
this can be identified by these conditions:
|
|
||||||
- the current flow rate is 0 mB/t and it should not be
|
|
||||||
- can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up
|
|
||||||
- can later identified by presence of steam in tank with a 0 flow rate
|
|
||||||
]]--
|
|
||||||
local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01
|
local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01
|
||||||
self.db.annunciator.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0
|
annunc.TurbineTrip[idx] = has_steam and db.state.flow_rate == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- update auto control ready state for this unit
|
-- update auto control ready state for this unit
|
||||||
@@ -577,6 +567,7 @@ end
|
|||||||
---@param self _unit_self unit instance
|
---@param self _unit_self unit instance
|
||||||
function logic.update_status_text(self)
|
function logic.update_status_text(self)
|
||||||
local AISTATE = self.types.AISTATE
|
local AISTATE = self.types.AISTATE
|
||||||
|
local annunc = self.db.annunciator
|
||||||
|
|
||||||
-- check if an alarm is active (tripped or ack'd)
|
-- check if an alarm is active (tripped or ack'd)
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -666,13 +657,13 @@ function logic.update_status_text(self)
|
|||||||
if plc_db.mek_status.status then
|
if plc_db.mek_status.status then
|
||||||
self.status_text[1] = "ACTIVE"
|
self.status_text[1] = "ACTIVE"
|
||||||
|
|
||||||
if self.db.annunciator.ReactorHighDeltaT then
|
if annunc.ReactorHighDeltaT then
|
||||||
self.status_text[2] = "core temperature rising"
|
self.status_text[2] = "core temperature rising"
|
||||||
elseif self.db.annunciator.ReactorTempHigh then
|
elseif annunc.ReactorTempHigh then
|
||||||
self.status_text[2] = "core temp high, system nominal"
|
self.status_text[2] = "core temp high, system nominal"
|
||||||
elseif self.db.annunciator.FuelInputRateLow then
|
elseif annunc.FuelInputRateLow then
|
||||||
self.status_text[2] = "insufficient fuel input rate"
|
self.status_text[2] = "insufficient fuel input rate"
|
||||||
elseif self.db.annunciator.WasteLineOcclusion then
|
elseif annunc.WasteLineOcclusion then
|
||||||
self.status_text[2] = "insufficient waste output rate"
|
self.status_text[2] = "insufficient waste output rate"
|
||||||
elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
|
elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
|
||||||
self.status_text[2] = "awaiting flow stability"
|
self.status_text[2] = "awaiting flow stability"
|
||||||
@@ -711,7 +702,7 @@ function logic.update_status_text(self)
|
|||||||
end
|
end
|
||||||
|
|
||||||
self.status_text = { "RPS SCRAM", cause }
|
self.status_text = { "RPS SCRAM", cause }
|
||||||
elseif self.db.annunciator.RadiationWarning then
|
elseif annunc.RadiationWarning then
|
||||||
-- elevated, non-hazardous level of radiation is low priority, so display it now if everything else was fine
|
-- elevated, non-hazardous level of radiation is low priority, so display it now if everything else was fine
|
||||||
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
|
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
|
||||||
else
|
else
|
||||||
@@ -726,7 +717,7 @@ function logic.update_status_text(self)
|
|||||||
self.status_text[2] = "core hot"
|
self.status_text[2] = "core hot"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif self.db.annunciator.RadiationWarning then
|
elseif annunc.RadiationWarning then
|
||||||
-- in case PLC was disconnected but radiation is present
|
-- in case PLC was disconnected but radiation is present
|
||||||
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
|
self.status_text = { "RADIATION DETECTED", "elevated level of radiation" }
|
||||||
else
|
else
|
||||||
@@ -738,6 +729,7 @@ end
|
|||||||
---@param self _unit_self unit instance
|
---@param self _unit_self unit instance
|
||||||
function logic.handle_redstone(self)
|
function logic.handle_redstone(self)
|
||||||
local AISTATE = self.types.AISTATE
|
local AISTATE = self.types.AISTATE
|
||||||
|
local annunc = self.db.annunciator
|
||||||
|
|
||||||
-- check if an alarm is active (tripped or ack'd)
|
-- check if an alarm is active (tripped or ack'd)
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@@ -806,7 +798,7 @@ function logic.handle_redstone(self)
|
|||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
local enable_emer_cool = self.plc_cache.rps_status.low_cool or
|
local enable_emer_cool = self.plc_cache.rps_status.low_cool or
|
||||||
(self.auto_engaged and self.db.annunciator.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp))
|
(self.auto_engaged and annunc.CoolantLevelLow and is_active(self.alarms.ReactorOverTemp))
|
||||||
|
|
||||||
-- don't turn off emergency coolant on sufficient coolant level since it might drop again
|
-- don't turn off emergency coolant on sufficient coolant level since it might drop again
|
||||||
-- turn off once system is OK again
|
-- turn off once system is OK again
|
||||||
@@ -822,7 +814,7 @@ function logic.handle_redstone(self)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.db.annunciator.EmergencyCoolant > 1 and self.emcool_opened then
|
if annunc.EmergencyCoolant > 1 and self.emcool_opened then
|
||||||
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed"))
|
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed"))
|
||||||
log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam"))
|
log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam"))
|
||||||
end
|
end
|
||||||
@@ -849,7 +841,7 @@ function logic.handle_redstone(self)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.db.annunciator.EmergencyCoolant > 1 and not self.emcool_opened then
|
if annunc.EmergencyCoolant > 1 and not self.emcool_opened then
|
||||||
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened"))
|
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened"))
|
||||||
log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam"))
|
log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam"))
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user