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