Merge pull request #281 from MikaylaFischler/devel

2023.07.16 Release
This commit is contained in:
Mikayla 2023-07-16 21:25:00 -04:00 committed by GitHub
commit 9bd79dacad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2497 additions and 808 deletions

171
ccmsi.lua
View File

@ -20,7 +20,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.5a" local CCMSI_VERSION = "v1.7d"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
@ -44,11 +44,15 @@ local function get_opt(opt, options)
return nil return nil
end 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 -- ask the user yes or no
local function ask_y_n(question, default) local function ask_y_n(question, default)
print(question) print(question)
if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end 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 if response == "" then return default
elseif response == "Y" or response == "y" then return true elseif response == "Y" or response == "y" then return true
elseif response == "N" or response == "n" then return false elseif response == "N" or response == "n" then return false
@ -56,13 +60,13 @@ local function ask_y_n(question, default)
end end
-- print out a white + blue text message -- print out a white + blue text message
local function pkg_message(message, package) white(); print(message .. " "); blue(); println(package); white() end local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
-- indicate actions to be taken based on package differences for installs/updates -- indicate actions to be taken based on package differences for installs/updates
local function show_pkg_change(name, v_local, v_remote) local function show_pkg_change(name, v_local, v_remote)
if v_local ~= nil then if v_local ~= nil then
if v_local ~= v_remote 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 elseif mode == "install" then
pkg_message("[" .. name .. "] reinstalling", v_local) pkg_message("[" .. name .. "] reinstalling", v_local)
end end
@ -87,15 +91,13 @@ end
local function get_remote_manifest() local function get_remote_manifest()
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)
if response == nil then if response == nil then
orange(); println("failed to get installation manifest from GitHub, cannot update or install") orange();println("failed to get installation manifest from GitHub, cannot update or install")
red(); println("HTTP error: " .. error); white() red();println("HTTP error: " .. error);white()
return false, {} return false, {}
end end
local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end) local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end)
if not ok then if not ok then red();println("error parsing remote installation manifest");white() end
red(); println("error parsing remote installation manifest"); white()
end
return ok, manifest return ok, manifest
end end
@ -107,7 +109,7 @@ local function write_install_manifest(manifest, dependencies)
local is_dependency = false local is_dependency = false
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if (key == "bootloader" and dependency == "system") or key == dependency then if (key == "bootloader" and dependency == "system") or key == dependency then
is_dependency = true; break is_dependency = true;break
end end
end end
if key == app or key == "comms" or is_dependency then versions[key] = value 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() imfile.close()
end 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 -- get and validate command line options
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --") 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(" update - update files EXCEPT for config/logs")
println(" remove - delete files EXCEPT for config/logs") println(" remove - delete files EXCEPT for config/logs")
println(" purge - delete files INCLUDING config/logs") println(" purge - delete files INCLUDING config/logs")
white(); println("<app>"); lgray() white();println("<app>");lgray()
println(" reactor-plc - reactor PLC firmware") println(" reactor-plc - reactor PLC firmware")
println(" rtu - RTU firmware") println(" rtu - RTU firmware")
println(" supervisor - supervisor server application") println(" supervisor - supervisor server application")
println(" coordinator - coordinator application") println(" coordinator - coordinator application")
println(" pocket - pocket application") println(" pocket - pocket application")
white(); println("<branch>"); yellow() white();println("<branch>");yellow()
println(" second parameter when used with check") println(" second parameter when used with check")
lgray(); println(" main (default) | latest | devel"); white() lgray();println(" main (default) | latest | devel");white()
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" }) mode = get_opt(opts[1], { "check", "install", "update", "remove", "purge" })
if mode == nil then if mode == nil then
red(); println("Unrecognized mode."); white() red();println("Unrecognized mode.");white()
return return
end end
app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" }) app = get_opt(opts[2], { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" })
if app == nil and mode ~= "check" then if app == nil and mode ~= "check" then
red(); println("Unrecognized application."); white() red();println("Unrecognized application.");white()
return return
end end
-- determine target -- determine target
if mode == "check" then target = opts[2] else target = opts[3] end if mode == "check" then target = opts[2] else target = opts[3] end
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then if (target ~= "main") and (target ~= "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" target = "main"
end end
@ -179,7 +253,7 @@ if mode == "check" then
local local_ok, local_manifest = read_local_manifest() local local_ok, local_manifest = read_local_manifest()
if not local_ok then 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 } } local_manifest = { versions = { installer = CCMSI_VERSION } }
else else
local_manifest.versions.installer = CCMSI_VERSION local_manifest.versions.installer = CCMSI_VERSION
@ -190,16 +264,16 @@ if mode == "check" then
term.setTextColor(colors.purple) term.setTextColor(colors.purple)
print(string.format("%-14s", "[" .. key .. "]")) print(string.format("%-14s", "[" .. key .. "]"))
if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then if key == "installer" or (local_ok and (local_manifest.versions[key] ~= nil)) then
blue(); print(local_manifest.versions[key]) blue();print(local_manifest.versions[key])
if value ~= local_manifest.versions[key] then if value ~= local_manifest.versions[key] then
white(); print(" (") white();print(" (")
term.setTextColor(colors.cyan) term.setTextColor(colors.cyan)
print(value); white(); println(" available)") print(value);white();println(" available)")
else green(); println(" (up to date)") end else green();println(" (up to date)") end
else else
lgray(); print("not installed"); white(); print(" (latest ") lgray();print("not installed");white();print(" (latest ")
term.setTextColor(colors.cyan) term.setTextColor(colors.cyan)
print(value); white(); println(")") print(value);white();println(")")
end end
end end
elseif mode == "install" or mode == "update" then 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() local local_ok, local_manifest = read_local_manifest()
if not local_ok then if not local_ok then
if mode == "update" 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 return
end end
else else
@ -229,13 +303,13 @@ elseif mode == "install" or mode == "update" then
ver.lockbox.v_local = local_manifest.versions.lockbox ver.lockbox.v_local = local_manifest.versions.lockbox
if local_manifest.versions[app] == nil then 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 return
end end
local_manifest.versions.installer = CCMSI_VERSION local_manifest.versions.installer = CCMSI_VERSION
if manifest.versions.installer ~= CCMSI_VERSION then 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
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) show_pkg_change("comms", ver.comms.v_local, ver.comms.v_remote)
ver.comms.changed = 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 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 end
-- display graphics version change information -- 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 ver.lockbox.changed = ver.lockbox.v_local ~= ver.lockbox.v_remote
-- ask for confirmation -- ask for confirmation
if not ask_y_n("Continue?", false) then return end if not ask_y_n("Continue", false) then return end
-------------------------- --------------------------
-- START INSTALL/UPDATE -- -- START INSTALL/UPDATE --
@ -302,7 +376,7 @@ elseif mode == "install" or mode == "update" then
-- check space constraints -- check space constraints
if space_available < space_required then if space_available < space_required then
single_file_mode = true 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.") 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 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 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) local dl, err = http.get(repo_path .. file)
if dl == nil then if dl == nil then
red(); println("GET HTTP Error " .. err) red();println("GET HTTP Error " .. err)
success = false success = false
break break
else else
@ -384,10 +458,13 @@ elseif mode == "install" or mode == "update" then
if mode == "install" then if mode == "install" then
println("Installation completed successfully.") println("Installation completed successfully.")
else println("Update completed successfully.") end 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 else
if mode == "install" then if mode == "install" then
red(); println("Installation failed.") red();println("Installation failed.")
else orange(); println("Update failed, existing files unmodified.") end else orange();println("Update failed, existing files unmodified.") end
end end
else else
-- go through all files and replace one by one -- 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) local dl, err = http.get(repo_path .. file)
if dl == nil then if dl == nil then
red(); println("GET HTTP Error " .. err) red();println("GET HTTP Error " .. err)
success = false success = false
break break
else else
@ -424,6 +501,9 @@ elseif mode == "install" or mode == "update" then
if mode == "install" then if mode == "install" then
println("Installation completed successfully.") println("Installation completed successfully.")
else println("Update completed successfully.") end 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 else
red() red()
if mode == "install" then if mode == "install" then
@ -434,10 +514,10 @@ elseif mode == "install" or mode == "update" then
elseif mode == "remove" or mode == "purge" then elseif mode == "remove" or mode == "purge" then
local ok, manifest = read_local_manifest() local ok, manifest = read_local_manifest()
if not ok then if not ok then
red(); println("Error parsing local installation manifest."); white() red();println("Error parsing local installation manifest.");white()
return return
elseif mode == "remove" and manifest.versions[app] == nil then 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 return
end end
@ -449,7 +529,10 @@ elseif mode == "remove" or mode == "purge" then
end end
-- ask for confirmation -- ask for confirmation
if not ask_y_n("Continue?", false) then return end if not ask_y_n("Continue", false) then return end
-- delete unused files first
clean(manifest)
local file_list = manifest.files local file_list = manifest.files
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
@ -469,9 +552,9 @@ elseif mode == "remove" or mode == "purge" then
end) end)
if not log_deleted then if not log_deleted then
red(); println("failed to delete log file") red();println("failed to delete log file")
white(); println("press enter to continue...") white();println("press any key to continue...")
read(); lgray() any_key();lgray()
end end
end end
@ -480,10 +563,7 @@ elseif mode == "remove" or mode == "purge" then
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "purge" or file ~= config_file then if mode == "purge" or file ~= config_file then
if fs.exists(file) then if fs.exists(file) then fs.delete(file);println("deleted " .. file) end
fs.delete(file)
println("deleted " .. file)
end
end end
end end
@ -508,8 +588,7 @@ elseif mode == "remove" or mode == "purge" then
end end
if folder ~= app and fs.isDir(folder) then if folder ~= app and fs.isDir(folder) then
fs.delete(folder) fs.delete(folder);println("deleted app subdirectory " .. folder)
println("deleted app subdirectory " .. folder)
end end
end end
end end
@ -527,7 +606,7 @@ elseif mode == "remove" or mode == "purge" then
imfile.close() imfile.close()
end end
green(); println("Done!") green();println("Done!")
end end
white() white()

View File

@ -2,6 +2,7 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local types = require("scada-common.types")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process") local process = require("coordinator.process")
@ -12,7 +13,6 @@ local dialog = require("coordinator.ui.dialog")
local print = util.print local print = util.print
local println = util.println local println = util.println
local println_ts = util.println_ts
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE 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 UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND local FAC_COMMAND = comms.FAC_COMMAND
local LINK_TIMEOUT = 60.0
local coordinator = {} local coordinator = {}
-- request the user to select a monitor -- 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_seq_num = 0,
sv_r_seq_num = nil, sv_r_seq_num = nil,
sv_config_err = false, sv_config_err = false,
connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW, 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) comms.set_trusted_range(range)
@ -295,77 +300,78 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
---@class coord_comms ---@class coord_comms
local public = {} 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 -- close the connection to the server
function public.close() function public.close()
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_addr = comms.BROADCAST self.sv_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil self.sv_r_seq_num = nil
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end 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 -- send a facility command
---@param cmd FAC_COMMAND command ---@param cmd FAC_COMMAND command
function public.send_fac_command(cmd) ---@param option any? optional option options for the optional options (like waste mode)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd }) function public.send_fac_command(cmd, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd, option })
end end
-- send the auto process control configuration with a start command -- 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 -- send a unit command
---@param cmd UNIT_COMMAND command ---@param cmd UNIT_COMMAND command
---@param unit integer unit ID ---@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) function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option }) _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end end
@ -424,7 +430,10 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
-- handle a packet -- handle a packet
---@param packet mgmt_frame|crdn_frame|capi_frame|nil ---@param packet mgmt_frame|crdn_frame|capi_frame|nil
---@return boolean close_ui
function public.handle_packet(packet) function public.handle_packet(packet)
local was_linked = self.sv_linked
if packet ~= nil then if packet ~= nil then
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local r_chan = packet.scada_frame.remote_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 if l_chan ~= crd_channel then
log.debug("received packet on unconfigured channel " .. l_chan, true) log.debug("received packet on unconfigured channel " .. l_chan, true)
elseif r_chan == pkt_channel then elseif r_chan == 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 ---@cast packet capi_frame
-- look for an associated session -- look for an associated session
local session = apisessions.find_session(src_addr) 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 elseif dev_type == DEVICE_TYPE.PKT then
-- pocket linking request -- pocket linking request
local id = apisessions.establish_session(src_addr, firmware_v) 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)) 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) _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 -- check sequence number
if self.sv_r_seq_num == nil then if self.sv_r_seq_num == nil then
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
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()) 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 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?") log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
return return false
else else
self.sv_r_seq_num = packet.scada_frame.seq_num() self.sv_r_seq_num = packet.scada_frame.seq_num()
end end
@ -563,6 +573,10 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
end end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack) 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 else
log.debug(util.c("received facility command ack with unknown command ", cmd)) log.debug(util.c("received facility command ack with unknown command ", cmd))
end end
@ -627,70 +641,7 @@ function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel,
end end
elseif protocol == PROTOCOL.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame ---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if self.sv_linked 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 packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 then 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_addr = comms.BROADCAST
self.sv_linked = false self.sv_linked = false
self.sv_r_seq_num = nil 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") log.info("server connection closed by remote host")
else else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type) log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end 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 else
log.debug("discarding non-link SCADA_MGMT packet before linked") log.debug("discarding non-link SCADA_MGMT packet before linked")
end 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) log.debug("received packet for unknown channel " .. r_chan, true)
end end
end end
return was_linked and not self.sv_linked
end end
-- check if the coordinator is still linked to the supervisor -- check if the coordinator is still linked to the supervisor

View File

@ -10,9 +10,15 @@ local util = require("scada-common.util")
local process = require("coordinator.process") local process = require("coordinator.process")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local pgi = require("coordinator.ui.pgi")
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local PROCESS = types.PROCESS 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 = {} local iocontrol = {}
---@class ioctl ---@class ioctl
@ -27,6 +33,19 @@ local function __generic_ack(success) end
-- luacheck: unused args -- 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 -- initialize the coordinator IO controller
---@param conf facility_conf configuration ---@param conf facility_conf configuration
---@param comms coord_comms comms reference ---@param comms coord_comms comms reference
@ -52,6 +71,10 @@ function iocontrol.init(conf, comms)
gen_fault = false 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(), radiation = types.new_zero_radiation_reading(),
save_cfg_ack = __generic_ack, save_cfg_ack = __generic_ack,
@ -65,16 +88,21 @@ function iocontrol.init(conf, comms)
induction_ps_tbl = {}, induction_ps_tbl = {},
induction_data_tbl = {}, induction_data_tbl = {},
sps_ps_tbl = {},
sps_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {},
env_d_ps = psil.create(), env_d_ps = psil.create(),
env_d_data = {} env_d_data = {}
} }
-- create induction tables (currently only 1 is supported) -- create induction and SPS tables (currently only 1 of each is supported)
for _ = 1, conf.num_units do table.insert(io.facility.induction_ps_tbl, psil.create())
local data = {} ---@type imatrix_session_db table.insert(io.facility.induction_data_tbl, {})
table.insert(io.facility.induction_ps_tbl, psil.create()) table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, data) table.insert(io.facility.sps_data_tbl, {})
end
io.units = {} io.units = {}
for i = 1, conf.num_units do for i = 1, conf.num_units do
@ -87,11 +115,15 @@ function iocontrol.init(conf, comms)
num_boilers = 0, num_boilers = 0,
num_turbines = 0, num_turbines = 0,
num_snas = 0,
control_state = false, control_state = false,
burn_rate_cmd = 0.0, burn_rate_cmd = 0.0,
waste_control = 0,
radiation = types.new_zero_radiation_reading(), 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 -- auto control group
a_group = 0, a_group = 0,
@ -100,10 +132,10 @@ function iocontrol.init(conf, comms)
scram = function () process.scram(i) end, scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end, reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(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_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_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, start_ack = __generic_ack,
scram_ack = __generic_ack, scram_ack = __generic_ack,
@ -152,7 +184,10 @@ function iocontrol.init(conf, comms)
boiler_data_tbl = {}, boiler_data_tbl = {},
turbine_ps_tbl = {}, turbine_ps_tbl = {},
turbine_data_tbl = {} turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
} }
-- create boiler tables -- create boiler tables
@ -179,6 +214,92 @@ function iocontrol.init(conf, comms)
process.init(io, comms) process.init(io, comms)
end 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 -- populate facility structure builds
---@param build table ---@param build table
---@return boolean valid ---@return boolean valid
@ -191,21 +312,29 @@ function iocontrol.record_facility_builds(build)
-- induction matricies -- induction matricies
if type(build.induction) == "table" then if type(build.induction) == "table" then
for id, matrix in pairs(build.induction) do for id, matrix in pairs(build.induction) do
if type(fac.induction_data_tbl[id]) == "table" then if not _record_multiblock_build(id, matrix, fac.induction_data_tbl, fac.induction_ps_tbl) 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
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id)) log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
valid = false valid = false
end end
end 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 else
log.debug("facility builds not a table") log.debug("facility builds not a table")
valid = false valid = false
@ -249,16 +378,7 @@ function iocontrol.record_unit_builds(builds)
-- boiler builds -- boiler builds
if type(build.boilers) == "table" then if type(build.boilers) == "table" then
for b_id, boiler in pairs(build.boilers) do for b_id, boiler in pairs(build.boilers) do
if type(unit.boiler_data_tbl[b_id]) == "table" then if not _record_multiblock_build(b_id, boiler, unit.boiler_data_tbl, unit.boiler_ps_tbl) 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
log.debug(util.c(log_header, "invalid boiler id ", b_id)) log.debug(util.c(log_header, "invalid boiler id ", b_id))
valid = false valid = false
end end
@ -268,27 +388,49 @@ function iocontrol.record_unit_builds(builds)
-- turbine builds -- turbine builds
if type(build.turbines) == "table" then if type(build.turbines) == "table" then
for t_id, turbine in pairs(build.turbines) do for t_id, turbine in pairs(build.turbines) do
if type(unit.turbine_data_tbl[t_id]) == "table" then if not _record_multiblock_build(t_id, turbine, unit.turbine_data_tbl, unit.turbine_ps_tbl) 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
log.debug(util.c(log_header, "invalid turbine id ", t_id)) log.debug(util.c(log_header, "invalid turbine id ", t_id))
valid = false valid = false
end end
end 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
end end
return valid return valid
end 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 -- update facility status
---@param status table ---@param status table
---@return boolean valid ---@return boolean valid
@ -306,7 +448,7 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1] 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.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2] 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]) io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
end end
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 else
log.debug(log_header .. "control status not a table or length mismatch") log.debug(log_header .. "control status not a table or length mismatch")
valid = false valid = false
@ -390,36 +538,23 @@ function iocontrol.update_facility_status(status)
for id, matrix in pairs(rtu_statuses.induction) do for id, matrix in pairs(rtu_statuses.induction) do
if type(fac.induction_data_tbl[id]) == "table" then if type(fac.induction_data_tbl[id]) == "table" then
local rtu_faulted = matrix[1] ---@type boolean local data = fac.induction_data_tbl[id] ---@type imatrix_session_db
fac.induction_data_tbl[id].formed = matrix[2] ---@type boolean local ps = fac.induction_ps_tbl[id] ---@type psil
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 rtu_faulted = _record_multiblock_status(matrix, data, ps)
fac.induction_ps_tbl[id].publish("formed", data.formed) if rtu_faulted then
fac.induction_ps_tbl[id].publish("faulted", rtu_faulted) ps.publish("computed_status", 3) -- faulted
elseif data.formed then
if data.formed then if data.tanks.energy_fill >= 0.99 then
if rtu_faulted then ps.publish("computed_status", 6) -- full
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
elseif data.tanks.energy_fill <= 0.01 then 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 else
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line ps.publish("computed_status", 4) -- on-line
end end
else else
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed ps.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)
end end
else else
log.debug(util.c(log_header, "invalid induction matrix id ", id)) log.debug(util.c(log_header, "invalid induction matrix id ", id))
@ -430,6 +565,82 @@ function iocontrol.update_facility_status(status)
valid = false valid = false
end 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 -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then if #rtu_statuses.rad_mon > 0 then
@ -472,6 +683,9 @@ function iocontrol.update_unit_statuses(statuses)
valid = false valid = false
else else
local burn_rate_sum = 0.0 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 -- get all unit statuses
for i = 1, #statuses do for i = 1, #statuses do
@ -480,6 +694,8 @@ function iocontrol.update_unit_statuses(statuses)
local unit = io.units[i] ---@type ioctl_unit local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i] local status = statuses[i]
local burn_rate = 0.0
if type(status) ~= "table" or #status ~= 5 then if type(status) ~= "table" or #status ~= 5 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
valid = false valid = false
@ -515,7 +731,8 @@ function iocontrol.update_unit_statuses(statuses)
-- if status hasn't been received, mek_status = {} -- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then 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 end
if unit.reactor_data.mek_status.status then 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 for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then if type(unit.boiler_data_tbl[id]) == "table" then
local rtu_faulted = boiler[1] ---@type boolean local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean local ps = unit.boiler_ps_tbl[id] ---@type psil
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 rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.boiler_ps_tbl[id].publish("formed", data.formed)
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
elseif data.formed then elseif data.formed then
if data.state.boil_rate > 0 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 else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle ps.publish("computed_status", 4) -- idle
end end
else else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed ps.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)
end end
else else
log.debug(util.c(log_header, "invalid boiler id ", id)) 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 for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then if type(unit.turbine_data_tbl[id]) == "table" then
local 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 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) local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then if rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted ps.publish("computed_status", 3) -- faulted
elseif data.formed then elseif data.formed then
if data.tanks.energy_fill >= 0.99 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 elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle ps.publish("computed_status", 4) -- idle
else else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active ps.publish("computed_status", 5) -- active
end end
else else
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed ps.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)
end end
else else
log.debug(util.c(log_header, "invalid turbine id ", id)) log.debug(util.c(log_header, "invalid turbine id ", id))
@ -662,6 +853,58 @@ function iocontrol.update_unit_statuses(statuses)
valid = false valid = false
end 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 -- environment detector status
if type(rtu_statuses.rad_mon) == "table" then if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then if #rtu_statuses.rad_mon > 0 then
@ -739,12 +982,17 @@ function iocontrol.update_unit_statuses(statuses)
local unit_state = status[5] local unit_state = status[5]
if type(unit_state) == "table" then 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_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2]) 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[3])
unit.unit_ps.publish("U_AutoReady", unit_state[4]) unit.unit_ps.publish("U_AutoDegraded", unit_state[4])
unit.unit_ps.publish("U_AutoDegraded", unit_state[5]) 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 else
log.debug(log_header .. "unit state length mismatch") log.debug(log_header .. "unit state length mismatch")
valid = false valid = false
@ -753,10 +1001,18 @@ function iocontrol.update_unit_statuses(statuses)
log.debug(log_header .. "unit state not a table") log.debug(log_header .. "unit state not a table")
valid = false valid = false
end 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
end end
io.facility.ps.publish("burn_sum", burn_rate_sum) 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 -- update alarm sounder
sounder.eval(io.units) sounder.eval(io.units)
@ -765,6 +1021,8 @@ function iocontrol.update_unit_statuses(statuses)
return valid return valid
end end
--#endregion
-- get the IO controller database -- get the IO controller database
function iocontrol.get_db() return io end function iocontrol.get_db() return io end

View File

@ -11,6 +11,7 @@ local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local PRODUCT = types.WASTE_PRODUCT
---@class process_controller ---@class process_controller
local process = {} local process = {}
@ -24,7 +25,9 @@ local self = {
burn_target = 0.0, burn_target = 0.0,
charge_target = 0.0, charge_target = 0.0,
gen_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") log.error("process.init(): failed to load coordinator settings file")
end end
-- facility auto control configuration
local config = settings.get("PROCESS") ---@type coord_auto_config|nil local config = settings.get("PROCESS") ---@type coord_auto_config|nil
if type(config) == "table" then if type(config) == "table" then
self.config.mode = config.mode self.config.mode = config.mode
self.config.burn_target = config.burn_target self.config.burn_target = config.burn_target
self.config.charge_target = config.charge_target self.config.charge_target = config.charge_target
self.config.gen_target = config.gen_target self.config.gen_target = config.gen_target
self.config.limits = config.limits 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_mode", self.config.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target) 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_charge_target", self.config.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_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 for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit local unit = self.io.units[id] ---@type ioctl_unit
@ -70,18 +77,18 @@ function process.init(iocontrol, coord_comms)
log.info("PROCESS: loaded auto control settings from coord.settings") log.info("PROCESS: loaded auto control settings from coord.settings")
end end
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil -- unit waste states
local waste_modes = settings.get("WASTE_MODES") ---@type table|nil
if type(waste_mode) == "table" then if type(waste_modes) == "table" then
for id, mode in pairs(waste_mode) do for id, mode in pairs(waste_modes) do
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
end end
log.info("PROCESS: loaded waste mode settings from coord.settings") log.info("PROCESS: loaded unit waste mode settings from coord.settings")
end end
-- unit priority groups
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
if type(prio_groups) == "table" then if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do for id, group in pairs(prio_groups) do
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
@ -137,7 +144,7 @@ end
-- set waste mode -- set waste mode
---@param id integer unit ID ---@param id integer unit ID
---@param mode integer waste mode ---@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 -- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode) 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) settings.set("WASTE_MODES", waste_mode)
if not settings.save("/coord.settings") then 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
end end
@ -204,6 +211,24 @@ end
-- AUTO PROCESS CONTROL -- -- 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 -- stop automatic process control
function process.stop_auto() function process.stop_auto()
self.comms.send_fac_command(FAC_COMMAND.STOP) self.comms.send_fac_command(FAC_COMMAND.STOP)
@ -216,6 +241,30 @@ function process.start_auto()
log.debug("PROCESS: START AUTO CTL") log.debug("PROCESS: START AUTO CTL")
end 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 -- save process control settings
---@param mode PROCESS control mode ---@param mode PROCESS control mode
---@param burn_target number burn rate target ---@param burn_target number burn rate target
@ -223,29 +272,17 @@ end
---@param gen_target number generation rate target ---@param gen_target number generation rate target
---@param limits table unit burn rate limits ---@param limits table unit burn rate limits
function process.save(mode, burn_target, charge_target, gen_target, limits) function process.save(mode, burn_target, charge_target, gen_target, limits)
-- attempt to load settings log.debug("PROCESS: SAVE")
if not settings.load("/coord.settings") then
log.warning("process.save(): failed to load coordinator settings file")
end
-- config table -- update config table
self.config = { self.config.mode = mode
mode = mode, self.config.burn_target = burn_target
burn_target = burn_target, self.config.charge_target = charge_target
charge_target = charge_target, self.config.gen_target = gen_target
gen_target = gen_target, self.config.limits = limits
limits = limits
}
-- save config -- save config
settings.set("PROCESS", self.config) self.io.facility.save_cfg_ack(_write_auto_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)
end end
-- handle a start command acknowledgement -- handle a start command acknowledgement
@ -258,16 +295,33 @@ function process.start_ack_handle(response)
self.config.charge_target = response[4] self.config.charge_target = response[4]
self.config.gen_target = response[5] 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] 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 end
self.io.facility.ps.publish("auto_mode", self.config.mode) self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("burn_target", self.config.burn_target) self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("charge_target", self.config.charge_target) self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("gen_target", self.config.gen_target) self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
self.io.facility.start_ack(ack) self.io.facility.start_ack(ack)
end 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 return process

View File

@ -5,8 +5,12 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local 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 main_view = require("coordinator.ui.layout.main_view")
local unit_view = require("coordinator.ui.layout.unit_view") local unit_view = require("coordinator.ui.layout.unit_view")
@ -21,7 +25,9 @@ local engine = {
monitors = nil, ---@type monitors_struct|nil monitors = nil, ---@type monitors_struct|nil
dmesg_window = nil, ---@type table|nil dmesg_window = nil, ---@type table|nil
ui_ready = false, ui_ready = false,
fp_ready = false,
ui = { ui = {
front_panel = nil, ---@type graphics_element|nil
main_display = nil, ---@type graphics_element|nil main_display = nil, ---@type graphics_element|nil
unit_displays = {} unit_displays = {}
} }
@ -46,24 +52,10 @@ end
---@param monitors monitors_struct ---@param monitors monitors_struct
function renderer.set_displays(monitors) function renderer.set_displays(monitors)
engine.monitors = monitors engine.monitors = monitors
end
-- check if the renderer is configured to use a given monitor peripheral -- report to front panel as connected
---@nodiscard iocontrol.fp_monitor_state(0, true)
---@param periph table peripheral for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
---@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
end end
-- init all displays in use by the renderer -- 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 for _, monitor in ipairs(engine.monitors.unit_displays) do
_init_display(monitor) _init_display(monitor)
end 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 end
-- check main display width -- check main display width
@ -109,6 +112,51 @@ function renderer.init_dmesg()
log.direct_dmesg(engine.dmesg_window) log.direct_dmesg(engine.dmesg_window)
end 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 -- start the coordinator GUI
function renderer.start_ui() function renderer.start_ui()
if not engine.ui_ready then if not engine.ui_ready then
@ -116,13 +164,15 @@ function renderer.start_ui()
engine.dmesg_window.setVisible(false) engine.dmesg_window.setVisible(false)
-- show main view on main monitor -- show main view on main monitor
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root} if engine.monitors.primary ~= nil then
main_view(engine.ui.main_display) 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 -- show unit views on unit displays
for i = 1, #engine.monitors.unit_displays do for idx, display in pairs(engine.monitors.unit_displays) do
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root} engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
unit_view(engine.ui.unit_displays[i], i) unit_view(engine.ui.unit_displays[idx], idx)
end end
-- start flasher callback task -- start flasher callback task
@ -135,12 +185,14 @@ end
-- close out the UI -- close out the UI
function renderer.close_ui() function renderer.close_ui()
-- stop blinking indicators if not engine.fp_ready then
flasher.clear() -- stop blinking indicators
flasher.clear()
end
-- delete element trees -- delete element trees
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end 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 -- report ui as not ready
engine.ui_ready = false engine.ui_ready = false
@ -157,22 +209,121 @@ function renderer.close_ui()
engine.dmesg_window.redraw() engine.dmesg_window.redraw()
end end
-- is the front panel ready?
---@nodiscard
---@return boolean ready
function renderer.fp_ready() return engine.fp_ready end
-- is the UI ready? -- is the UI ready?
---@nodiscard ---@nodiscard
---@return boolean ready ---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end 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 -- handle a touch event
---@param event mouse_interaction|nil ---@param event mouse_interaction|nil
function renderer.handle_mouse(event) function renderer.handle_mouse(event)
if engine.ui_ready and event ~= nil then if event ~= nil then
if event.monitor == engine.monitors.primary_name then if engine.fp_ready and event.monitor == "terminal" then
engine.ui.main_display.handle_mouse(event) engine.ui.front_panel.handle_mouse(event)
else elseif engine.ui_ready then
for id, monitor in ipairs(engine.monitors.unit_name_map) do if event.monitor == engine.monitors.primary_name then
if event.monitor == monitor then engine.ui.main_display.handle_mouse(event)
local layout = engine.ui.unit_displays[id] ---@type graphics_element else
layout.handle_mouse(event) 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 end
end end

View File

@ -1,11 +1,12 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("coordinator.config") local config = require("coordinator.config")
local iocontrol = require("coordinator.iocontrol")
local pocket = require("coordinator.session.pocket") local pocket = require("coordinator.session.pocket")
local apisessions = {} local apisessions = {}
@ -112,6 +113,7 @@ function apisessions.establish_session(source_addr, version)
setmetatable(pkt_s, mt) setmetatable(pkt_s, mt)
iocontrol.fp_pkt_connected(id, version, source_addr)
log.debug(util.c("[API] established new session: ", pkt_s)) log.debug(util.c("[API] established new session: ", pkt_s))
self.next_id = id + 1 self.next_id = id + 1

View File

@ -1,7 +1,9 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local pocket = {} local pocket = {}
@ -9,8 +11,6 @@ local PROTOCOL = comms.PROTOCOL
-- local CAPI_TYPE = comms.CAPI_TYPE -- local CAPI_TYPE = comms.CAPI_TYPE
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local println = util.println
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000 -- local RETRY_PERIOD = 1000
@ -69,6 +69,7 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
iocontrol.fp_pkt_disconnected(id)
end end
-- send a CAPI packet -- 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 RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms") -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
iocontrol.fp_pkt_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
@ -172,7 +175,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println("connection to pocket session " .. id .. " closed by server")
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
@ -211,7 +213,6 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
println("connection to pocket session " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_header .. "session closed by remote host")
return self.connected return self.connected
end end

View File

@ -4,6 +4,7 @@
require("/initenv").init_env() require("/initenv").init_env()
local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") local log = require("scada-common.log")
local network = require("scada-common.network") local network = require("scada-common.network")
@ -21,7 +22,7 @@ local sounder = require("coordinator.sounder")
local apisessions = require("coordinator.session.apisessions") local apisessions = require("coordinator.session.apisessions")
local COORDINATOR_VERSION = "v0.17.1" local COORDINATOR_VERSION = "v0.21.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -30,7 +31,6 @@ local log_graphics = coordinator.log_graphics
local log_sys = coordinator.log_sys local log_sys = coordinator.log_sys
local log_boot = coordinator.log_boot local log_boot = coordinator.log_boot
local log_comms = coordinator.log_comms local log_comms = coordinator.log_comms
local log_comms_connecting = coordinator.log_comms_connecting
local log_crypto = coordinator.log_crypto local log_crypto = coordinator.log_crypto
---------------------------------------- ----------------------------------------
@ -80,6 +80,9 @@ local function main()
-- mount connected devices -- mount connected devices
ppm.mount_all() ppm.mount_all()
-- report versions/init fp PSIL
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
-- setup monitors -- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured or monitors == nil then if not configured or monitors == nil then
@ -127,6 +130,7 @@ local function main()
sounder.init(speaker, config.SOUNDER_VOLUME) sounder.init(speaker, config.SOUNDER_VOLUME)
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms") log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
log_sys("annunciator alarm configured") log_sys("annunciator alarm configured")
iocontrol.fp_has_speaker(true)
end end
---------------------------------------- ----------------------------------------
@ -148,6 +152,7 @@ local function main()
return return
else else
log_comms("wireless modem connected") log_comms("wireless modem connected")
iocontrol.fp_has_modem(true)
end end
-- create connection watchdog -- create connection watchdog
@ -167,76 +172,54 @@ local function main()
local loop_clock = util.new_clock(MAIN_CLOCK) 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 log_graphics("starting front panel UI...")
local function init_connect_sv()
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_CHANNEL)
-- attempt to establish a connection with the supervisory computer local fp_ok, fp_message = pcall(renderer.start_fp)
if not coord_comms.sv_connect(60, tick_waiting, task_done) then if not fp_ok then
log_sys("supervisor connection failed, shutting down...") renderer.close_fp()
log.fatal("failed to connect to supervisor") log_graphics(util.c("front panel UI error: ", fp_message))
return false println_ts("front panel UI creation failed")
end log.fatal(util.c("front panel GUI render failed with error ", fp_message))
return true
end
if not init_connect_sv() then
println("startup> failed to connect to supervisor")
log_sys("system shutdown")
return return
else else log_graphics("front panel ready") end
log_sys("supervisor connected, proceeding to UI start")
end
---------------------------------------- -- start up the main UI
-- start the UI
----------------------------------------
-- start up the UI
---@return boolean ui_ok started ok ---@return boolean ui_ok started ok
local function init_start_ui() local function start_main_ui()
log_graphics("starting UI...") log_graphics("starting main UI...")
local draw_start = util.time_ms() 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 if not ui_ok then
renderer.close_ui() renderer.close_ui()
log_graphics(util.c("UI crashed: ", message)) log_graphics(util.c("main UI error: ", ui_message))
println_ts("UI crashed") log.fatal(util.c("main GUI render failed with error ", ui_message))
log.fatal(util.c("GUI crashed with error ", message))
else else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
-- start clock
loop_clock.start()
end end
return ui_ok return ui_ok
end end
local ui_ok = init_start_ui()
---------------------------------------- ----------------------------------------
-- main event loop -- 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") 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 clock
-- start connection watchdog loop_clock.start()
conn_watchdog.feed()
log.debug("startup> conn watchdog started")
log_sys("system started successfully") log_sys("system started successfully")
end
-- main event loop -- main event loop
while ui_ok do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- handle event
@ -250,31 +233,32 @@ local function main()
if nic.is_modem(device) then if nic.is_modem(device) then
nic.disconnect() nic.disconnect()
log_sys("comms modem disconnected") log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
-- close out UI local other_modem = ppm.get_wireless_modem()
renderer.close_ui() 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 -- alert user to status
log_sys("awaiting comms modem reconnect...") log_sys("awaiting comms modem reconnect...")
iocontrol.fp_has_modem(false)
end
else else
log_sys("non-comms modem disconnected") log_sys("non-comms modem disconnected")
end end
elseif type == "monitor" then elseif type == "monitor" then
if renderer.is_monitor_used(device) then if renderer.handle_disconnect(device) then
---@todo will be handled properly in #249 log_sys("lost a configured monitor")
-- "halt and catch fire" style handling
local msg = "lost a configured monitor, system will now exit"
println_ts(msg)
log_sys(msg)
break
else else
log_sys("lost unused monitor, ignoring") log_sys("lost an unused monitor")
end end
elseif type == "speaker" then elseif type == "speaker" then
local msg = "lost alarm sounder speaker" log_sys("lost alarm sounder speaker")
println_ts(msg) iocontrol.fp_has_speaker(false)
log_sys(msg)
end end
end end
elseif event == "peripheral" then elseif event == "peripheral" then
@ -282,33 +266,50 @@ local function main()
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
nic.connect(device)
log_sys("comms modem reconnected") log_sys("comms modem reconnected")
println_ts("wireless modem reconnected.") nic.connect(device)
iocontrol.fp_has_modem(true)
-- re-init system elseif device.isWireless() then
if not init_connect_sv() then break end log.info("unused wireless modem reconnected")
ui_ok = init_start_ui()
else else
log_sys("wired modem reconnected") log_sys("wired modem reconnected")
end end
-- elseif type == "monitor" then elseif type == "monitor" then
---@todo will be handled properly in #249 if renderer.handle_reconnect(param1, device) then
-- not supported, system will exit on loss of in-use monitors log_sys(util.c("configured monitor ", param1, " reconnected"))
else
log_sys(util.c("unused monitor ", param1, " connected"))
end
elseif type == "speaker" then elseif type == "speaker" then
local msg = "alarm sounder speaker reconnected" log_sys("alarm sounder speaker reconnected")
println_ts(msg)
log_sys(msg)
sounder.reconnect(device) sounder.reconnect(device)
iocontrol.fp_has_speaker(true)
end end
end end
elseif event == "timer" then elseif event == "timer" then
if loop_clock.is_clock(param1) then if loop_clock.is_clock(param1) then
-- main loop tick -- 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 -- iterate sessions
apisessions.iterate_all() apisessions.iterate_all()
@ -316,25 +317,19 @@ local function main()
apisessions.free_all_closed() apisessions.free_all_closed()
-- update date and time string for main display -- 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() loop_clock.start()
elseif conn_watchdog.is_timer(param1) then elseif conn_watchdog.is_timer(param1) then
-- supervisor watchdog timeout -- supervisor watchdog timeout
local msg = "supervisor server timeout" log_comms("supervisor server timeout")
log_comms(msg)
println_ts(msg)
-- close connection, UI, and stop sounder -- close connection, main UI, and stop sounder
coord_comms.close() coord_comms.close()
renderer.close_ui() renderer.close_ui()
sounder.stop() 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 else
-- a non-clock/main watchdog timer event -- a non-clock/main watchdog timer event
@ -347,25 +342,19 @@ local function main()
elseif event == "modem_message" then elseif event == "modem_message" then
-- got a packet -- got a packet
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5) local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
coord_comms.handle_packet(packet)
-- check if it was a disconnect -- handle then check if it was a disconnect
if not coord_comms.is_linked() then if coord_comms.handle_packet(packet) then
log_comms("supervisor closed connection") log_comms("supervisor closed connection")
-- close connection, UI, and stop sounder -- close connection, main UI, and stop sounder
coord_comms.close() coord_comms.close()
renderer.close_ui() renderer.close_ui()
sounder.stop() 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 end
elseif event == "monitor_touch" then elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
-- handle a monitor touch event event == "mouse_drag" or event == "mouse_scroll" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied -- handle speaker buffer emptied
@ -374,10 +363,17 @@ local function main()
-- check for termination request -- check for termination request
if event == "terminate" or ppm.should_terminate() then if event == "terminate" or ppm.should_terminate() then
println_ts("terminate requested, closing connections...") -- handle supervisor connection
log_comms("terminate requested, closing 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() coord_comms.close()
log_comms("supervisor connection closed") log_comms("supervisor connection closed")
-- handle API sessions
log_comms("closing api sessions...") log_comms("closing api sessions...")
apisessions.close_all() apisessions.close_all()
log_comms("api sessions closed") log_comms("api sessions closed")
@ -386,15 +382,20 @@ local function main()
end end
renderer.close_ui() renderer.close_ui()
renderer.close_fp()
sounder.stop() sounder.stop()
log_sys("system shutdown") 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") println_ts("exited")
log.info("exited") log.info("exited")
end end
if not xpcall(main, crash.handler) then if not xpcall(main, crash.handler) then
pcall(renderer.close_ui) pcall(renderer.close_ui)
pcall(renderer.close_fp)
pcall(sounder.stop) pcall(sounder.stop)
crash.exit() crash.exit()
else else

View File

@ -0,0 +1,48 @@
--
-- Pocket Connection Entry
--
local iocontrol = require("coordinator.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create a pocket list entry
---@param parent graphics_element parent
---@param id integer PKT session ID
local function init(parent, id)
local ps = iocontrol.get_db().fp.ps
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
local ps_prefix = "pkt_" .. id .. "_"
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)
return root
end
return init

View File

@ -15,8 +15,10 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data") local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad") local RadIndicator = require("graphics.elements.indicators.rad")
local StateIndicator = require("graphics.elements.indicators.state")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local Checkbox = require("graphics.elements.controls.checkbox")
local HazardButton = require("graphics.elements.controls.hazard_button") local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button") local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") 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 lu_cpair = cpair(colors.gray, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray) 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 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} 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 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 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 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) 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) 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() main.line_break()
@ -99,7 +103,7 @@ local function new_view(root, x, y)
-- process control -- -- 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 -- -- process control targets --
@ -148,46 +152,77 @@ local function new_view(root, x, y)
local rate_limits = {} local rate_limits = {}
for i = 1, facility.num_units do for i = 1, 4 do
local unit = units[i] ---@type ioctl_unit 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 _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} 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)} local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,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 = 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} 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) 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}
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(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 end
------------------- -------------------
-- unit statuses -- -- 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 for i = 1, 4 do
local unit = units[i] ---@type ioctl_unit 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 _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} 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 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,colors.gray)} 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,colors.gray),flash=true,period=period.BLINK_250_MS} 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) if i <= facility.num_units then
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update) 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 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 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) 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 for i = 1, #rate_limits do rate_limits[i].enable() end
end 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 end
return new_view return new_view

View File

@ -33,41 +33,21 @@ local border = core.border
local period = core.flasher.PERIOD 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 -- create a unit view
---@param parent graphics_element parent ---@param parent graphics_element parent
---@param id integer ---@param id integer
local function init(parent, id) local function init(parent, id)
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
local f_ps = iocontrol.get_db().facility.ps 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 u_ps = unit.unit_ps
local b_ps = unit.boiler_ps_tbl local b_ps = unit.boiler_ps_tbl
local t_ps = unit.turbine_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} 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) 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_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_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) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)

View File

@ -0,0 +1,121 @@
--
-- Coordinator Front Panel GUI
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local pgi = require("coordinator.ui.pgi")
local style = require("coordinator.ui.style")
local pkt_entry = require("coordinator.ui.components.pkt_entry")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local ListBox = require("graphics.elements.listbox")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local TabBar = require("graphics.elements.controls.tabbar")
local LED = require("graphics.elements.indicators.led")
local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-- create new front panel view
---@param panel graphics_element main displaybox
---@param num_units integer number of units (number of unit monitors)
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.fp.header}
local page_div = Div{parent=panel,x=1,y=3}
--
-- system indicators
--
local main_page = Div{parent=page_div,x=1,y=1}
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
status.update(true)
system.line_break()
heartbeat.register(ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
system.line_break()
modem.register(ps, "has_modem", modem.update)
network.register(ps, "link_state", network.update)
local speaker = LED{parent=system,label="SPEAKER",colors=cpair(colors.green,colors.green_off)}
speaker.register(ps, "has_speaker", speaker.update)
---@diagnostic disable-next-line: undefined-field
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=cpair(colors.green,colors.green_off)}
main_monitor.register(ps, "main_monitor", main_monitor.update)
monitors.line_break()
for i = 1, num_units do
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=cpair(colors.green,colors.green_off)}
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
end
--
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- page handling
--
-- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
-- assemble page panes
local panes = { main_page, api_page }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
local tabs = {
{ name = "CRD", color = cpair(colors.black, colors.ivory) },
{ name = "API", color = cpair(colors.black, colors.ivory) },
}
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
-- link pocket API list management to PGI
pgi.link_elements(api_list, pkt_entry)
end
return init

View File

@ -9,7 +9,7 @@ local iocontrol = require("coordinator.iocontrol")
local style = require("coordinator.ui.style") local style = require("coordinator.ui.style")
local imatrix = require("coordinator.ui.components.imatrix") 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 unit_overview = require("coordinator.ui.components.unit_overview")
local core = require("graphics.core") local core = require("graphics.core")

58
coordinator/ui/pgi.lua Normal file
View 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

View File

@ -10,6 +10,41 @@ local cpair = core.cpair
-- GLOBAL -- -- 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.root = cpair(colors.black, colors.lightGray)
style.header = cpair(colors.white, colors.gray) style.header = cpair(colors.white, colors.gray)
style.label = cpair(colors.gray, colors.lightGray) style.label = cpair(colors.gray, colors.lightGray)
@ -151,7 +186,90 @@ style.imatrix = {
{ {
color = cpair(colors.black, colors.yellow), color = cpair(colors.black, colors.yellow),
text = "HIGH CHARGE" text = "HIGH CHARGE"
}
}
}
style.sps = {
-- SPS states
states = {
{
color = cpair(colors.black, colors.yellow),
text = "OFF-LINE"
}, },
{
color = cpair(colors.black, colors.orange),
text = "NOT FORMED"
},
{
color = cpair(colors.black, colors.orange),
text = "RTU FAULT"
},
{
color = cpair(colors.black, colors.gray),
text = "IDLE"
},
{
color = cpair(colors.black, colors.green),
text = "ACTIVE"
}
}
}
style.waste = {
-- auto waste processing states
states = {
{
color = cpair(colors.black, colors.green),
text = "PLUTONIUM"
},
{
color = cpair(colors.black, colors.cyan),
text = "POLONIUM"
},
{
color = cpair(colors.black, colors.purple),
text = "ANTI MATTER"
}
},
states_abbrv = {
{
color = cpair(colors.black, colors.green),
text = "Pu"
},
{
color = cpair(colors.black, colors.cyan),
text = "Po"
},
{
color = cpair(colors.black, colors.purple),
text = "AM"
}
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{
text = "Auto",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.white, colors.gray)
},
{
text = "Pu",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.green)
},
{
text = "Po",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.cyan)
},
{
text = "AM",
fg_bg = cpair(colors.black, colors.lightGray),
active_fg_bg = cpair(colors.black, colors.purple)
}
} }
} }

View File

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

View File

@ -20,6 +20,7 @@ local element = {}
---@alias graphics_args graphics_args_generic ---@alias graphics_args graphics_args_generic
---|waiting_args ---|waiting_args
---|checkbox_args
---|hazard_button_args ---|hazard_button_args
---|multi_button_args ---|multi_button_args
---|push_button_args ---|push_button_args

View File

@ -8,7 +8,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -8,7 +8,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 ---@field hidden? boolean true to hide on initial draw
-- new color map -- new color map

View File

@ -0,0 +1,85 @@
-- Checkbox Graphics Element
local core = require("graphics.core")
local element = require("graphics.element")
---@class checkbox_args
---@field label string checkbox text
---@field box_fg_bg cpair colors for checkbox
---@field callback function function to call on press
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer auto incremented if omitted
---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw
-- new checkbox control
---@param args checkbox_args
---@return graphics_element element, element_id id
local function checkbox(args)
assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field")
assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field")
args.height = 1
args.width = 3 + string.len(args.label)
-- create new graphics element base object
local e = element.new(args)
e.value = false
-- show the button state
local function draw()
e.window.setCursorPos(1, 1)
if e.value then
-- show as selected
e.window.setTextColor(args.box_fg_bg.bkg)
e.window.setBackgroundColor(args.box_fg_bg.fgd)
e.window.write("\x88")
e.window.setTextColor(args.box_fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write("\x95")
else
-- show as unselected
e.window.setTextColor(e.fg_bg.bkg)
e.window.setBackgroundColor(args.box_fg_bg.bkg)
e.window.write("\x88")
e.window.setTextColor(args.box_fg_bg.bkg)
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write("\x95")
end
end
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled and core.events.was_clicked(event.type) then
e.value = not e.value
draw()
args.callback(e.value)
end
end
-- set the value
---@param val integer new value
function e.set_value(val)
e.value = val
draw()
end
-- write label text
e.window.setCursorPos(3, 1)
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
e.window.write(args.label)
-- initial draw
draw()
return e.complete()
end
return checkbox

View File

@ -14,7 +14,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -20,7 +20,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -16,7 +16,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -13,7 +13,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -17,7 +17,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -16,7 +16,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -12,7 +12,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -18,7 +18,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -6,7 +6,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
-- new core map box -- new core map box
---@nodiscard ---@nodiscard

View File

@ -14,7 +14,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -10,7 +10,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -16,7 +16,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -14,7 +14,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -9,7 +9,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -14,7 +14,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -13,7 +13,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -14,7 +14,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
---@field y? integer 1 if omitted ---@field y? integer auto incremented if omitted
---@field width integer length ---@field width integer length
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -15,7 +15,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -16,7 +16,7 @@ local flasher = require("graphics.flasher")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 fg_bg? cpair foreground/background colors
---@field hidden? boolean true to hide on initial draw ---@field hidden? boolean true to hide on initial draw

View File

@ -8,7 +8,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -15,7 +15,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -7,7 +7,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 ---@field hidden? boolean true to hide on initial draw
-- new pipe network -- new pipe network

View File

@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -13,7 +13,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -11,7 +11,7 @@ local element = require("graphics.element")
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@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 width? integer parent width if omitted
---@field height? integer parent height if omitted ---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height ---@field gframe? graphics_frame frame instead of x/y/width/height

View File

@ -43,8 +43,10 @@ end
-- start/resume the flasher periodic -- start/resume the flasher periodic
function flasher.run() function flasher.run()
active = true if not active then
callback_250ms() active = true
callback_250ms()
end
end end
-- clear all blinking indicators and stop the flasher periodic -- clear all blinking indicators and stop the flasher periodic

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ local coreio = require("pocket.coreio")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") 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 = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -112,7 +112,7 @@ local function main()
if not ui_ok then if not ui_ok then
renderer.close_ui() renderer.close_ui()
println(util.c("UI error: ", message)) 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 else
-- start clock -- start clock
loop_clock.start() loop_clock.start()

View File

@ -1,5 +1,5 @@
-- --
-- Main SCADA Coordinator GUI -- Reactor PLC Front Panel GUI
-- --
local types = require("scada-common.types") local types = require("scada-common.types")
@ -28,7 +28,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair local cpair = core.cpair
local border = core.border local border = core.border
-- create new main view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
local function init(panel) 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} local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}

View File

@ -12,6 +12,7 @@ local cpair = core.cpair
-- remap global colors -- remap global colors
colors.ivory = colors.pink colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown colors.red_off = colors.brown
colors.yellow_off = colors.magenta colors.yellow_off = colors.magenta
colors.green_off = colors.lime colors.green_off = colors.lime
@ -28,7 +29,7 @@ style.colors = {
{ c = colors.cyan, hex = 0x34bac8 }, { c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 }, { c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff }, { 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.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca }, -- { c = colors.white, hex = 0xdcd9ca },

View File

@ -929,47 +929,7 @@ function plc.comms(id, version, nic, plc_channel, svr_channel, range, reactor, r
---@cast packet mgmt_frame ---@cast packet mgmt_frame
-- if linked, only accept packets from configured supervisor -- if linked, only accept packets from configured supervisor
if self.linked then if self.linked then
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE 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
-- keep alive request received, echo back -- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1] local timestamp = packet.data[1]

View File

@ -19,7 +19,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.5.0" local R_PLC_VERSION = "v1.5.5"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -190,7 +190,7 @@ local function main()
renderer.close_ui() renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("init> running without front panel") 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") log.info("init> running in headless mode without front panel")
end end
end end

View File

@ -77,7 +77,7 @@ function threads.thread__main(smem, init)
loop_clock.start() loop_clock.start()
-- send updated data -- send updated data
if nic.connected() then if nic.is_connected() then
if plc_comms.is_linked() then if plc_comms.is_linked() then
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS) smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
else else
@ -116,7 +116,7 @@ function threads.thread__main(smem, init)
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- determine if we are still in a degraded state -- 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 plc_state.degraded = false
end end
@ -146,7 +146,7 @@ function threads.thread__main(smem, init)
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) 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 -- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then if packet ~= nil then
@ -177,16 +177,21 @@ function threads.thread__main(smem, init)
nic.disconnect() nic.disconnect()
println_ts("comms modem disconnected!") 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 if plc_state.init_ok then
-- try to scram reactor if it is still connected -- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
end end
plc_state.degraded = true
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@ -230,7 +235,7 @@ function threads.thread__main(smem, init)
rps.reset() rps.reset()
end end
elseif networked and type == "modem" then elseif networked and type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
plc_dev.modem = device plc_dev.modem = device
plc_state.no_modem = false plc_state.no_modem = false
@ -244,6 +249,8 @@ function threads.thread__main(smem, init)
if not plc_state.no_reactor then if not plc_state.no_reactor then
plc_state.degraded = false plc_state.degraded = false
end end
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end

View File

@ -2,7 +2,7 @@ local rtu = require("rtu.rtu")
local boilerv_rtu = {} local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device -- create new boiler device
---@nodiscard ---@nodiscard
---@param boiler table ---@param boiler table
---@return rtu_device interface, boolean faulted ---@return rtu_device interface, boolean faulted

48
rtu/dev/dynamicv_rtu.lua Normal file
View File

@ -0,0 +1,48 @@
local rtu = require("rtu.rtu")
local dynamicv_rtu = {}
-- create new dynamic tank device
---@nodiscard
---@param dynamic_tank table
---@return rtu_device interface, boolean faulted
function dynamicv_rtu.new(dynamic_tank)
local unit = rtu.init_unit()
-- disable auto fault clearing
dynamic_tank.__p_clear_fault()
dynamic_tank.__p_disable_afc()
-- discrete inputs --
unit.connect_di(dynamic_tank.isFormed)
-- coils --
unit.connect_coil(function () dynamic_tank.incrementContainerEditMode() end, function () end)
unit.connect_coil(function () dynamic_tank.decrementContainerEditMode() end, function () end)
-- input registers --
-- multiblock properties
unit.connect_input_reg(dynamic_tank.getLength)
unit.connect_input_reg(dynamic_tank.getWidth)
unit.connect_input_reg(dynamic_tank.getHeight)
unit.connect_input_reg(dynamic_tank.getMinPos)
unit.connect_input_reg(dynamic_tank.getMaxPos)
-- build properties
unit.connect_input_reg(dynamic_tank.getTankCapacity)
unit.connect_input_reg(dynamic_tank.getChemicalTankCapacity)
-- tanks/containers
unit.connect_input_reg(dynamic_tank.getStored)
unit.connect_input_reg(dynamic_tank.getFilledPercentage)
-- holding registers --
unit.connect_holding_reg(dynamic_tank.getContainerEditMode, dynamic_tank.setContainerEditMode)
-- check if any calls faulted
local faulted = dynamic_tank.__p_is_faulted()
dynamic_tank.__p_clear_fault()
dynamic_tank.__p_enable_afc()
return unit.interface(), faulted
end
return dynamicv_rtu

View File

@ -2,7 +2,7 @@ local rtu = require("rtu.rtu")
local turbinev_rtu = {} local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device -- create new turbine device
---@nodiscard ---@nodiscard
---@param turbine table ---@param turbine table
---@return rtu_device interface, boolean faulted ---@return rtu_device interface, boolean faulted

View File

@ -1,5 +1,5 @@
-- --
-- Main SCADA Coordinator GUI -- RTU Front Panel GUI
-- --
local types = require("scada-common.types") local types = require("scada-common.types")
@ -26,6 +26,7 @@ local UNIT_TYPE_LABELS = {
"REDSTONE", "REDSTONE",
"BOILER", "BOILER",
"TURBINE", "TURBINE",
"DYNAMIC TANK",
"IND MATRIX", "IND MATRIX",
"SPS", "SPS",
"SNA", "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 panel graphics_element main displaybox
---@param units table unit list ---@param units table unit list
local function init(panel, units) local function init(panel, units)

View File

@ -12,6 +12,7 @@ local cpair = core.cpair
-- remap global colors -- remap global colors
colors.ivory = colors.pink colors.ivory = colors.pink
colors.yellow_hc = colors.purple
colors.red_off = colors.brown colors.red_off = colors.brown
colors.yellow_off = colors.magenta colors.yellow_off = colors.magenta
colors.green_off = colors.lime colors.green_off = colors.lime
@ -28,7 +29,7 @@ style.colors = {
{ c = colors.cyan, hex = 0x34bac8 }, { c = colors.cyan, hex = 0x34bac8 },
{ c = colors.lightBlue, hex = 0x6cc0f2 }, { c = colors.lightBlue, hex = 0x6cc0f2 },
{ c = colors.blue, hex = 0x0096ff }, { 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.pink, hex = 0xdcd9ca }, -- IVORY
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
-- { c = colors.white, hex = 0xdcd9ca }, -- { c = colors.white, hex = 0xdcd9ca },

View File

@ -22,6 +22,7 @@ local rtu = require("rtu.rtu")
local threads = require("rtu.threads") local threads = require("rtu.threads")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") 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 envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu") local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local redstone_rtu = require("rtu.dev.redstone_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 sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.4.0" local RTU_VERSION = "v1.5.4"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@ -342,6 +343,18 @@ local function main()
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock")) log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
return false return false
end 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 elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
@ -464,7 +477,7 @@ local function main()
renderer.close_ui() renderer.close_ui()
println_ts(util.c("UI error: ", message)) println_ts(util.c("UI error: ", message))
println("startup> running without front panel") 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") log.info("startup> running in headless mode without front panel")
end end

View File

@ -10,6 +10,7 @@ local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") 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 envd_rtu = require("rtu.dev.envd_rtu")
local imatrix_rtu = require("rtu.dev.imatrix_rtu") local imatrix_rtu = require("rtu.dev.imatrix_rtu")
local sna_rtu = require("rtu.dev.sna_rtu") local sna_rtu = require("rtu.dev.sna_rtu")
@ -97,9 +98,15 @@ function threads.thread__main(smem)
nic.disconnect() nic.disconnect()
println_ts("wireless modem disconnected!") 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 else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@ -127,7 +134,7 @@ function threads.thread__main(smem)
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
if device.isWireless() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
nic.connect(device) nic.connect(device)
@ -135,6 +142,8 @@ function threads.thread__main(smem)
log.info("comms modem reconnected") log.info("comms modem reconnected")
databus.tx_hw_modem(true) databus.tx_hw_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end
@ -181,21 +190,22 @@ function threads.thread__main(smem)
databus.tx_unit_hw_type(unit.uid, unit.type) databus.tx_unit_hw_type(unit.uid, unit.type)
end 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 if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu, faulted = boilerv_rtu.new(device) 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) unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu, faulted = turbinev_rtu.new(device) 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) unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu, faulted = imatrix_rtu.new(device) 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) unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SPS then elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu, faulted = sps_rtu.new(device) 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) unit.formed = util.trinary(faulted, false, nil)
elseif unit.type == RTU_UNIT_TYPE.SNA then elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu, faulted = sna_rtu.new(device) 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.rtu, faulted = turbinev_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) 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 elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock -- induction matrix multiblock
unit.device = device unit.device = device

View File

@ -14,7 +14,7 @@ local max_distance = nil ---@type number|nil maximum acceptable t
---@class comms ---@class comms
local comms = {} local comms = {}
comms.version = "2.1.0" comms.version = "2.1.2"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@ -92,9 +92,11 @@ local PLC_AUTO_ACK = {
---@enum FAC_COMMAND ---@enum FAC_COMMAND
local FAC_COMMAND = { local FAC_COMMAND = {
SCRAM_ALL = 0, -- SCRAM all reactors SCRAM_ALL = 0, -- SCRAM all reactors
STOP = 1, -- stop automatic control STOP = 1, -- stop automatic process control
START = 2, -- start automatic control START = 2, -- start automatic process control
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units 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 ---@enum UNIT_COMMAND

View File

@ -20,7 +20,9 @@ local logger = {
mode = MODE.APPEND, mode = MODE.APPEND,
debug = false, debug = false,
file = nil, file = nil,
dmesg_out = nil dmesg_out = nil,
dmesg_restore_coord = { 1, 1 },
dmesg_scroll_count = 0
} }
---@type function ---@type function
@ -158,6 +160,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -193,6 +196,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -201,6 +205,8 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i]) out.write(lines[i])
end end
logger.dmesg_restore_coord = { out.getCursorPos() }
_log(util.c("[", t_stamp, "] [", tag, "] ", msg)) _log(util.c("[", t_stamp, "] [", tag, "] ", msg))
end end
@ -215,6 +221,7 @@ end
---@return function update, function done ---@return function update, function done
function log.dmesg_working(msg, tag, tag_color) function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(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 out = logger.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1 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 counter = 0
local function update(sec_remaining) 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 time = util.sprintf("%ds", sec_remaining)
local available = width - (string.len(time) + 2) local available = width - (string.len(time) + 2)
local progress = "" local progress = ""
out.setCursorPos(ts_coord.x1, ts_coord.y) out.setCursorPos(ts_coord.x1, new_y)
out.write(" ") out.write(" ")
if counter % 4 == 0 then if counter % 4 == 0 then
@ -249,10 +259,15 @@ function log.dmesg_working(msg, tag, tag_color)
out.setTextColor(initial_color) out.setTextColor(initial_color)
counter = counter + 1 counter = counter + 1
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
end end
local function done(ok) 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 if ok or ok == nil then
out.setTextColor(colors.green) out.setTextColor(colors.green)
@ -263,6 +278,8 @@ function log.dmesg_working(msg, tag, tag_color)
end end
out.setTextColor(initial_color) out.setTextColor(initial_color)
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
end end
return update, done return update, done

View File

@ -94,7 +94,7 @@ function network.nic(modem)
-- check if this NIC has a connected modem -- check if this NIC has a connected modem
---@nodiscard ---@nodiscard
function public.connected() return self.connected end function public.is_connected() return self.connected end
-- connect to a modem peripheral -- connect to a modem peripheral
---@param reconnected_modem table ---@param reconnected_modem table

View File

@ -89,16 +89,18 @@ types.RTU_UNIT_TYPE = {
REDSTONE = 1, -- redstone I/O REDSTONE = 1, -- redstone I/O
BOILER_VALVE = 2, -- boiler mekanism 10.1+ BOILER_VALVE = 2, -- boiler mekanism 10.1+
TURBINE_VALVE = 3, -- turbine, mekanism 10.1+ TURBINE_VALVE = 3, -- turbine, mekanism 10.1+
IMATRIX = 4, -- induction matrix DYNAMIC_VALVE = 4, -- dynamic tank, mekanism 10.1+
SPS = 5, -- SPS IMATRIX = 5, -- induction matrix
SNA = 6, -- SNA SPS = 6, -- SPS
ENV_DETECTOR = 7 -- environment detector SNA = 7, -- SNA
ENV_DETECTOR = 8 -- environment detector
} }
types.RTU_UNIT_NAMES = { types.RTU_UNIT_NAMES = {
"redstone", "redstone",
"boiler_valve", "boiler_valve",
"turbine_valve", "turbine_valve",
"dynamic_valve",
"induction_matrix", "induction_matrix",
"sps", "sps",
"sna", "sna",
@ -115,6 +117,7 @@ function types.rtu_type_to_string(utype)
elseif utype == types.RTU_UNIT_TYPE.REDSTONE or elseif utype == types.RTU_UNIT_TYPE.REDSTONE or
utype == types.RTU_UNIT_TYPE.BOILER_VALVE or utype == types.RTU_UNIT_TYPE.BOILER_VALVE or
utype == types.RTU_UNIT_TYPE.TURBINE_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.IMATRIX or
utype == types.RTU_UNIT_TYPE.SPS or utype == types.RTU_UNIT_TYPE.SPS or
utype == types.RTU_UNIT_TYPE.SNA or utype == types.RTU_UNIT_TYPE.SNA or
@ -158,13 +161,26 @@ types.PROCESS_NAMES = {
---@enum WASTE_MODE ---@enum WASTE_MODE
types.WASTE_MODE = { types.WASTE_MODE = {
AUTO = 1, AUTO = 1,
PLUTONIUM = 2, MANUAL_PLUTONIUM = 2,
POLONIUM = 3, MANUAL_POLONIUM = 3,
ANTI_MATTER = 4 MANUAL_ANTI_MATTER = 4
} }
types.WASTE_MODE_NAMES = { types.WASTE_MODE_NAMES = {
"AUTO", "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", "PLUTONIUM",
"POLONIUM", "POLONIUM",
"ANTI_MATTER" "ANTI_MATTER"
@ -315,6 +331,17 @@ types.RPS_TRIP_CAUSE = {
FORCE_DISABLED = "force_disabled" FORCE_DISABLED = "force_disabled"
} }
---@alias container_mode
---| "BOTH"
---| "FILL"
---| "EMPTY"
types.CONTAINER_MODE = {
BOTH = "BOTH",
FILL = "FILL",
EMPTY = "EMPTY"
}
---@alias dumping_mode ---@alias dumping_mode
---| "IDLE" ---| "IDLE"
---| "DUMPING" ---| "DUMPING"

View File

@ -11,6 +11,9 @@ local rsctl = require("supervisor.session.rsctl")
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES local PROCESS_NAMES = types.PROCESS_NAMES
local PRIO = types.ALARM_PRIORITY 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 local IO = rsio.IO
@ -59,8 +62,11 @@ function facility.new(num_reactors, cooling_conf)
all_sys_ok = false, all_sys_ok = false,
-- rtus -- rtus
rtu_conn_count = 0, rtu_conn_count = 0,
rtu_list = {},
redstone = {}, redstone = {},
induction = {}, induction = {},
sps = {},
tanks = {},
envd = {}, envd = {},
-- redstone I/O control -- redstone I/O control
io_ctl = nil, ---@type rs_controller io_ctl = nil, ---@type rs_controller
@ -99,6 +105,10 @@ function facility.new(num_reactors, cooling_conf)
last_update = 0, last_update = 0,
last_error = 0.0, last_error = 0.0,
last_time = 0.0, last_time = 0.0,
-- waste processing
waste_product = WASTE.PLUTONIUM,
current_waste_product = WASTE.PLUTONIUM,
pu_fallback = false,
-- statistics -- statistics
im_stat_init = false, im_stat_init = false,
avg_charge = util.mov_avg(3, 0.0), 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) table.insert(self.group_map, 0)
end 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 -- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone) 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 -- check if all auto-controlled units completed ramping
---@nodiscard ---@nodiscard
local function _all_units_ramped() local function _all_units_ramped()
@ -205,24 +212,50 @@ function facility.new(num_reactors, cooling_conf)
table.insert(self.redstone, rs_unit) table.insert(self.redstone, rs_unit)
end end
-- link an imatrix RTU session -- link an induction matrix RTU session
---@param imatrix unit_session ---@param imatrix unit_session
---@return boolean linked induction matrix accepted (max 1)
function public.add_imatrix(imatrix) 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 end
-- link an environment detector RTU session -- link an environment detector RTU session
---@param envd unit_session ---@param envd unit_session
---@return boolean linked environment detector accepted (max 1)
function public.add_envd(envd) 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 end
-- purge devices associated with the given RTU session ID -- purge devices associated with the given RTU session ID
---@param session integer RTU session ID ---@param session integer RTU session ID
function public.purge_rtu_devices(session) function public.purge_rtu_devices(session)
util.filter_table(self.redstone, 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
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)
end end
-- UPDATE -- -- UPDATE --
@ -236,9 +269,7 @@ function facility.new(num_reactors, cooling_conf)
-- update (iterate) the facility management -- update (iterate) the facility management
function public.update() function public.update()
-- unlink RTU unit sessions if they are closed -- unlink RTU unit sessions if they are closed
_unlink_disconnected_units(self.redstone) for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
_unlink_disconnected_units(self.induction)
_unlink_disconnected_units(self.envd)
-- current state for process control -- current state for process control
local charge_update = 0 local charge_update = 0
@ -277,6 +308,8 @@ function facility.new(num_reactors, cooling_conf)
-- Run Process Control -- -- Run Process Control --
------------------------- -------------------------
--#region Process Control
local avg_charge = self.avg_charge.compute() local avg_charge = self.avg_charge.compute()
local avg_inflow = self.avg_inflow.compute() local avg_inflow = self.avg_inflow.compute()
@ -542,10 +575,14 @@ function facility.new(num_reactors, cooling_conf)
next_mode = PROCESS.INACTIVE next_mode = PROCESS.INACTIVE
end end
--#endregion
------------------------------ ------------------------------
-- Evaluate Automatic SCRAM -- -- Evaluate Automatic SCRAM --
------------------------------ ------------------------------
--#region Automatic SCRAM
local astatus = self.ascram_status local astatus = self.ascram_status
if self.induction[1] ~= nil then if self.induction[1] ~= nil then
@ -659,6 +696,8 @@ function facility.new(num_reactors, cooling_conf)
end end
end end
--#endregion
-- update last mode and set next mode -- update last mode and set next mode
self.last_mode = self.mode self.last_mode = self.mode
self.mode = next_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) self.io_ctl.digital_write(IO.F_ALARM, has_alarm)
end 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 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() function public.update_units()
for i = 1, #self.units do for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit local u = self.units[i] ---@type reactor_unit
u.auto_set_waste(self.current_waste_product)
u.update() u.update()
end end
end end
@ -721,15 +781,15 @@ function facility.new(num_reactors, cooling_conf)
end end
-- stop auto control -- stop auto control
function public.auto_stop() function public.auto_stop() self.mode = PROCESS.INACTIVE end
self.mode = PROCESS.INACTIVE
end
-- set automatic control configuration and start the process -- set automatic control configuration and start the process
---@param config coord_auto_config configuration ---@param config coord_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating) ---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(config) 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 -- load up current limits
local limits = {} local limits = {}
@ -749,11 +809,11 @@ function facility.new(num_reactors, cooling_conf)
end end
if (type(config.charge_target) == "number") and config.charge_target >= 0 then 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 end
if (type(config.gen_target) == "number") and config.gen_target >= 0 then 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 end
if (type(config.limits) == "table") and (#config.limits == num_reactors) then 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 ready = self.mode_set > 0
if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) then if (self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0) or
ready = false (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) or
elseif (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0) then (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then
ready = false
elseif (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1) then
ready = false ready = false
end end
@ -782,7 +840,14 @@ function facility.new(num_reactors, cooling_conf)
if ready then self.mode = self.mode_set end if ready then self.mode = self.mode_set end
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 end
-- SETTINGS -- -- SETTINGS --
@ -807,15 +872,35 @@ function facility.new(num_reactors, cooling_conf)
end end
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 -- -- READ STATES/PROPERTIES --
-- get build properties of all machines -- get build properties of all facility devices
---@nodiscard ---@nodiscard
---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude ---@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(inc_imatrix) function public.get_build(type)
local all = type == nil
local build = {} local build = {}
if inc_imatrix ~= false then if all or type == RTU_UNIT_TYPE.IMATRIX then
build.induction = {} build.induction = {}
for i = 1, #self.induction do for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session local matrix = self.induction[i] ---@type unit_session
@ -823,6 +908,22 @@ function facility.new(num_reactors, cooling_conf)
end end
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 return build
end end
@ -844,7 +945,9 @@ function facility.new(num_reactors, cooling_conf)
astat.gen_fault or self.mode == PROCESS.GEN_RATE_FAULT_IDLE, astat.gen_fault or self.mode == PROCESS.GEN_RATE_FAULT_IDLE,
self.status_text[1], self.status_text[1],
self.status_text[2], 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 end
@ -866,23 +969,32 @@ function facility.new(num_reactors, cooling_conf)
-- status of induction matricies (including tanks) -- status of induction matricies (including tanks)
status.induction = {} status.induction = {}
for i = 1, #self.induction do for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session local matrix = self.induction[i] ---@type unit_session
status.induction[matrix.get_device_idx()] = { local db = matrix.get_db() ---@type imatrix_session_db
matrix.is_faulted(), status.induction[matrix.get_device_idx()] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
matrix.get_db().formed, end
matrix.get_db().state,
matrix.get_db().tanks -- 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 end
-- radiation monitors (environment detectors) -- radiation monitors (environment detectors)
status.rad_mon = {} status.rad_mon = {}
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
envd.is_faulted(),
envd.get_db().radiation
}
end end
return status return status

View File

@ -1,5 +1,5 @@
-- --
-- Main SCADA Coordinator GUI -- Supervisor Front Panel GUI
-- --
local util = require("scada-common.util") local util = require("scada-common.util")
@ -29,7 +29,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair local cpair = core.cpair
-- create new main view -- create new front panel view
---@param panel graphics_element main displaybox ---@param panel graphics_element main displaybox
local function init(panel) local function init(panel)
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header} TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}

View File

@ -1,7 +1,6 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
@ -16,8 +15,6 @@ local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND local FAC_COMMAND = comms.FAC_COMMAND
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local SV_Q_DATA = svqtypes.SV_Q_DATA local SV_Q_DATA = svqtypes.SV_Q_DATA
-- retry time constants in ms -- 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 elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
facility.ack_all() facility.ack_all()
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true }) _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 else
log.debug(log_header .. "CRDN facility command unknown") log.debug(log_header .. "CRDN facility command unknown")
end end
@ -294,9 +303,9 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
end end
elseif cmd == UNIT_COMMAND.SET_WASTE then 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 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 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 end
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_all() unit.ack_all()
@ -394,7 +403,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
local builds = {} local builds = {}
local unit = self.units[unit_id] ---@type reactor_unit 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 }) _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then 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 builds = {}
local unit = self.units[unit_id] ---@type reactor_unit 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 }) _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
else 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.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD
self.acks.fac_builds = false 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 end
else else
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true) log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true)

View File

@ -313,26 +313,31 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
if pkt.type == RPLC_TYPE.STATUS then if pkt.type == RPLC_TYPE.STATUS then
-- status packet received, update data -- status packet received, update data
if pkt.length >= 5 then if pkt.length >= 5 then
self.sDB.last_status_update = pkt.data[1] if (type(pkt.data[1]) == "number") and (type(pkt.data[2]) == "boolean") and (type(pkt.data[3]) == "boolean") and
self.sDB.control_state = pkt.data[2] (type(pkt.data[4]) == "boolean") and (type(pkt.data[5]) == "number") then
self.sDB.no_reactor = pkt.data[3] self.sDB.last_status_update = pkt.data[1]
self.sDB.formed = pkt.data[4] self.sDB.control_state = pkt.data[2]
self.sDB.auto_ack_token = pkt.data[5] 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 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 self.sDB.mek_status.heating_rate = pkt.data[6] or 0.0
-- attempt to read mek_data table -- attempt to read mek_data table
if pkt.data[7] ~= nil then if type(pkt.data[7]) == "table" then
local status = pcall(_copy_status, pkt.data[7]) local status = pcall(_copy_status, pkt.data[7])
if status then if status then
-- copied in status data OK -- copied in status data OK
self.received_status_cache = true self.received_status_cache = true
else else
-- error copying status data -- error copying status data
log.error(log_header .. "failed to parse status packet data") log.error(log_header .. "failed to parse status packet data")
end
end end
end end
else
log.debug(log_header .. "RPLC status packet invalid")
end end
else else
log.debug(log_header .. "RPLC status packet length mismatch") log.debug(log_header .. "RPLC status packet length mismatch")

View File

@ -11,6 +11,7 @@ local svqtypes = require("supervisor.session.svqtypes")
-- supervisor rtu sessions (svrs) -- supervisor rtu sessions (svrs)
local unit_session = require("supervisor.session.rtu.unit_session") local unit_session = require("supervisor.session.rtu.unit_session")
local svrs_boilerv = require("supervisor.session.rtu.boilerv") 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_envd = require("supervisor.session.rtu.envd")
local svrs_imatrix = require("supervisor.session.rtu.imatrix") local svrs_imatrix = require("supervisor.session.rtu.imatrix")
local svrs_redstone = require("supervisor.session.rtu.redstone") 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 -- turbine
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q) unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end 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 elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
-- environment detector -- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) 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 elseif u_type == RTU_UNIT_TYPE.SPS then
-- super-critical phase shifter -- super-critical phase shifter
unit = svrs_sps.new(id, i, unit_advert, self.modbus_q) unit = svrs_sps.new(id, i, unit_advert, self.modbus_q)
elseif u_type == RTU_UNIT_TYPE.SNA then if type(unit) ~= "nil" then facility.add_sps(unit) end
-- solar neutron activator elseif u_type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q) -- 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 elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
-- environment detector -- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)

View File

@ -0,0 +1,289 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes")
local unit_session = require("supervisor.session.rtu.unit_session")
local dynamicv = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local CONTAINER_MODE = types.CONTAINER_MODE
local MODBUS_FCODE = types.MODBUS_FCODE
local DTV_RTU_S_CMDS = qtypes.DTV_RTU_S_CMDS
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
local TXN_TYPES = {
FORMED = 1,
BUILD = 2,
STATE = 3,
TANKS = 4,
INC_CONT = 5,
DEC_CONT = 6,
SET_CONT = 7
}
local TXN_TAGS = {
"dynamicv.formed",
"dynamicv.build",
"dynamicv.state",
"dynamicv.tanks",
"dynamicv.inc_cont_mode",
"dynamicv.dec_cont_mode",
"dynamicv.set_cont_mode"
}
local PERIODICS = {
FORMED = 2000,
BUILD = 1000,
STATE = 1000,
TANKS = 500
}
-- create a new dynamicv rtu session runner
---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function dynamicv.new(session_id, unit_id, advert, out_queue)
-- type check
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
log.error("attempt to instantiate dynamicv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
local log_tag = "session.rtu(" .. session_id .. ").dynamicv(" .. advert.index .. "): "
local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
next_state_req = 0,
next_tanks_req = 0
},
---@class dynamicv_session_db
db = {
formed = false,
build = {
last_update = 0,
length = 0,
width = 0,
height = 0,
min_pos = types.new_zero_coordinate(),
max_pos = types.new_zero_coordinate(),
tank_capacity = 0,
chem_tank_capacity = 0
},
state = {
last_update = 0,
container_mode = CONTAINER_MODE.BOTH ---@type container_mode
},
tanks = {
last_update = 0,
stored = types.new_empty_gas(),
fill = 0
}
}
}
local public = self.session.get()
-- PRIVATE FUNCTIONS --
-- increment the container mode
local function _inc_cont_mode()
-- write coil 1 with unused value 0
self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
end
-- decrement the container mode
local function _dec_cont_mode()
-- write coil 2 with unused value 0
self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
end
-- set the container mode
---@param mode container_mode
local function _set_cont_mode(mode)
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
end
-- query if the multiblock is formed
local function _request_formed()
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
end
-- query the build of the device
local function _request_build()
-- read input registers 1 through 7 (start = 1, count = 7)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
end
-- query the state of the device
local function _request_state()
-- read holding register 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 })
end
-- query the tanks of the device
local function _request_tanks()
-- read input registers 8 through 9 (start = 8, count = 2)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
end
-- PUBLIC FUNCTIONS --
-- handle a packet
---@param m_pkt modbus_frame
function public.handle_packet(m_pkt)
local txn_type = self.session.try_resolve(m_pkt)
if txn_type == false then
-- nothing to do
elseif txn_type == TXN_TYPES.FORMED then
-- formed response
-- load in data if correct length
if m_pkt.length == 1 then
self.db.formed = m_pkt.data[1]
if not self.db.formed then self.has_build = false end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.BUILD then
-- build response
if m_pkt.length == 7 then
self.db.build.last_update = util.time_ms()
self.db.build.length = m_pkt.data[1]
self.db.build.width = m_pkt.data[2]
self.db.build.height = m_pkt.data[3]
self.db.build.min_pos = m_pkt.data[4]
self.db.build.max_pos = m_pkt.data[5]
self.db.build.tank_capacity = m_pkt.data[6]
self.db.build.chem_tank_capacity = m_pkt.data[7]
self.has_build = true
out_queue.push_data(unit_session.RTU_US_DATA.BUILD_CHANGED, { unit = advert.reactor, type = advert.type })
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.STATE then
-- state response
if m_pkt.length == 1 then
self.db.state.last_update = util.time_ms()
self.db.state.container_mode = m_pkt.data[1]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.TANKS then
-- tanks response
if m_pkt.length == 2 then
self.db.tanks.last_update = util.time_ms()
self.db.tanks.stored = m_pkt.data[1]
self.db.tanks.fill = m_pkt.data[2]
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
elseif txn_type == TXN_TYPES.INC_CONT or txn_type == TXN_TYPES.DEC_CONT or txn_type == TXN_TYPES.SET_CONT then
-- successful acknowledgement
elseif txn_type == nil then
log.error(log_tag .. "unknown transaction reply")
else
log.error(log_tag .. "unknown transaction type " .. txn_type)
end
end
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
-- check command queue
while self.session.in_q.ready() do
-- get a new message to process
local msg = self.session.in_q.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- instruction
local cmd = msg.message
if cmd == DTV_RTU_S_CMDS.INC_CONT_MODE then
_inc_cont_mode()
elseif cmd == DTV_RTU_S_CMDS.DEC_CONT_MODE then
_dec_cont_mode()
else
log.debug(util.c(log_tag, "unrecognized in-queue command ", cmd))
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- instruction with body
local cmd = msg.message ---@type queue_data
if cmd.key == DTV_RTU_S_DATA.SET_CONT_MODE then
if cmd.val == types.CONTAINER_MODE.BOTH or
cmd.val == types.CONTAINER_MODE.FILL or
cmd.val == types.CONTAINER_MODE.EMPTY then
_set_cont_mode(cmd.val)
else
log.debug(util.c(log_tag, "unrecognized container mode \"", cmd.val, "\""))
end
else
log.debug(util.c(log_tag, "unrecognized in-queue data ", cmd.key))
end
end
end
-- max 100ms spent processing queue
if util.time() - time_now > 100 then
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
time_now = util.time()
-- handle periodics
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
self.session.post_update()
end
-- invalidate build cache
function public.invalidate_cache()
self.periodics.next_formed_req = 0
self.periodics.next_build_req = 0
self.has_build = false
end
-- get the unit session database
---@nodiscard
function public.get_db() return self.db end
return public
end
return dynamicv

View File

@ -1,16 +1,31 @@
---@class rtu_unit_qtypes ---@class rtu_unit_qtypes
local qtypes = {} local qtypes = {}
-- turbine valve rtu session commands
local TBV_RTU_S_CMDS = { local TBV_RTU_S_CMDS = {
INC_DUMP_MODE = 1, INC_DUMP_MODE = 1,
DEC_DUMP_MODE = 2 DEC_DUMP_MODE = 2
} }
-- turbine valve rtu session commands w/ parameters
local TBV_RTU_S_DATA = { local TBV_RTU_S_DATA = {
SET_DUMP_MODE = 1 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_CMDS = TBV_RTU_S_CMDS
qtypes.TBV_RTU_S_DATA = TBV_RTU_S_DATA 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 return qtypes

View File

@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v0.18.0" local SUPERVISOR_VERSION = "v0.20.2"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -116,7 +116,7 @@ local function main()
if not fp_ok then if not fp_ok then
renderer.close_ui() renderer.close_ui()
println_ts(util.c("UI error: ", message)) 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 else
-- redefine println_ts local to not print as we have the front panel running -- redefine println_ts local to not print as we have the front panel running
println_ts = function (_) end println_ts = function (_) end
@ -153,7 +153,13 @@ local function main()
println_ts("wireless modem disconnected!") 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 else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@ -164,7 +170,7 @@ local function main()
if type ~= nil and device ~= nil then if type ~= nil and device ~= nil then
if type == "modem" then if type == "modem" then
if device.isWireless() and not nic.connected() then if device.isWireless() and not nic.is_connected() then
-- reconnected modem -- reconnected modem
nic.connect(device) nic.connect(device)
@ -172,6 +178,8 @@ local function main()
log.info("comms modem reconnected") log.info("comms modem reconnected")
databus.tx_hw_modem(true) databus.tx_hw_modem(true)
elseif device.isWireless() then
log.info("unused wireless modem reconnected")
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end end

View File

@ -11,11 +11,13 @@ local rsctl = require("supervisor.session.rsctl")
---@class reactor_control_unit ---@class reactor_control_unit
local unit = {} local unit = {}
local WASTE_MODE = types.WASTE_MODE local WASTE_MODE = types.WASTE_MODE
local ALARM = types.ALARM local WASTE = types.WASTE_PRODUCT
local PRIO = types.ALARM_PRIORITY local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE local PRIO = types.ALARM_PRIORITY
local TRI_FAIL = types.TRI_FAIL 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 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, num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
-- rtus -- rtus
rtu_list = {},
redstone = {}, redstone = {},
boilers = {}, boilers = {},
turbines = {}, turbines = {},
tanks = {},
snas = {},
envd = {}, envd = {},
sna_prod_rate = 0,
-- redstone control -- redstone control
io_ctl = nil, ---@type rs_controller io_ctl = nil, ---@type rs_controller
valves = {}, ---@type unit_valves valves = {}, ---@type unit_valves
@ -89,7 +95,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
damage_start = 0, damage_start = 0,
damage_last = 0, damage_last = 0,
damage_est_last = 0, damage_est_last = 0,
waste_mode = WASTE_MODE.AUTO, waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
status_text = { "UNKNOWN", "awaiting connection..." }, status_text = { "UNKNOWN", "awaiting connection..." },
-- logic for alarms -- logic for alarms
had_reactor = false, had_reactor = false,
@ -221,11 +227,15 @@ function unit.new(reactor_id, num_boilers, num_turbines)
degraded = false, degraded = false,
blade_count = 0, blade_count = 0,
br100 = 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 -- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone) self.io_ctl = rsctl.new(self.redstone)
@ -341,14 +351,34 @@ function unit.new(reactor_id, num_boilers, num_turbines)
emer_cool = emer_cool 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 if product == WASTE.PLUTONIUM then
---@param sessions table -- route through plutonium generation
local function _unlink_disconnected_units(sessions) waste_pu.open()
util.filter_table(sessions, function (u) return u.is_connected() end) 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 end
--#endregion
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class reactor_unit ---@class reactor_unit
@ -378,11 +408,12 @@ function unit.new(reactor_id, num_boilers, num_turbines)
table.insert(self.redstone, rs_unit) table.insert(self.redstone, rs_unit)
-- send or re-send waste settings -- send or re-send waste settings
public.set_waste(self.waste_mode) _set_waste_valves(self.waste_product)
end end
-- link a turbine RTU session -- link a turbine RTU session
---@param turbine unit_session ---@param turbine unit_session
---@return boolean linked turbine accepted to associated device slot
function public.add_turbine(turbine) function public.add_turbine(turbine)
if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then
table.insert(self.turbines, turbine) 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()) _reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx())
return true return true
else else return false end
return false
end
end end
-- link a boiler RTU session -- link a boiler RTU session
---@param boiler unit_session ---@param boiler unit_session
---@return boolean linked boiler accepted to associated device slot
function public.add_boiler(boiler) function public.add_boiler(boiler)
if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then
table.insert(self.boilers, boiler) 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()) _reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx())
return true return true
else else return false end
return false
end
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 -- link an environment detector RTU session
---@param envd unit_session ---@param envd unit_session
---@return boolean linked environment detector accepted (max 1)
function public.add_envd(envd) 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 end
-- purge devices associated with the given RTU session ID -- purge devices associated with the given RTU session ID
---@param session integer RTU session ID ---@param session integer RTU session ID
function public.purge_rtu_devices(session) function public.purge_rtu_devices(session)
util.filter_table(self.redstone, 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
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)
end end
--#endregion --#endregion
@ -445,10 +488,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
-- unlink RTU unit sessions if they are closed -- unlink RTU unit sessions if they are closed
_unlink_disconnected_units(self.redstone) for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
_unlink_disconnected_units(self.boilers)
_unlink_disconnected_units(self.turbines)
_unlink_disconnected_units(self.envd)
-- update degraded state for auto control -- update degraded state for auto control
self.db.control.degraded = (#self.boilers ~= num_boilers) or (#self.turbines ~= num_turbines) or (self.plc_i == nil) 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
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 --#endregion
-- OPERATIONS -- -- OPERATIONS --
@ -621,34 +670,18 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
end end
-- route reactor waste -- set waste processing mode
---@param mode WASTE_MODE waste handling mode ---@param mode WASTE_MODE processing mode
function public.set_waste(mode) function public.set_waste_mode(mode)
if mode == WASTE_MODE.AUTO then self.db.control.waste_mode = mode
---@todo automatic waste routing
self.waste_mode = mode if mode == WASTE_MODE.MANUAL_PLUTONIUM then
elseif mode == WASTE_MODE.PLUTONIUM then _set_waste_valves(WASTE.PLUTONIUM)
-- route through plutonium generation elseif mode == WASTE_MODE.MANUAL_POLONIUM then
self.waste_mode = mode _set_waste_valves(WASTE.POLONIUM)
waste_pu.open() elseif mode == WASTE_MODE.MANUAL_ANTI_MATTER then
waste_sna.close() _set_waste_valves(WASTE.ANTI_MATTER)
waste_po.close() elseif mode > WASTE_MODE.MANUAL_ANTI_MATTER then
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
log.debug(util.c("invalid waste mode setting ", mode)) log.debug(util.c("invalid waste mode setting ", mode))
end end
end end
@ -686,21 +719,25 @@ function unit.new(reactor_id, num_boilers, num_turbines)
return false return false
end 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 ---@nodiscard
---@param inc_plc boolean? true/nil to include PLC build, false to exclude ---@param filter -1|RTU_UNIT_TYPE? filter as described above
---@param inc_boilers boolean? true/nil to include boiler builds, false to exclude function public.get_build(filter)
---@param inc_turbines boolean? true/nil to include turbine builds, false to exclude local all = filter == nil
function public.get_build(inc_plc, inc_boilers, inc_turbines)
local build = {} local build = {}
if inc_plc ~= false then if all or (filter == -1) then
if self.plc_i ~= nil then if self.plc_i ~= nil then
build.reactor = self.plc_i.get_struct() build.reactor = self.plc_i.get_struct()
end end
end end
if inc_boilers ~= false then if all or (filter == RTU_UNIT_TYPE.BOILER_VALVE) then
build.boilers = {} build.boilers = {}
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
@ -708,7 +745,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
end end
if inc_turbines ~= false then if all or (filter == RTU_UNIT_TYPE.TURBINE_VALVE) then
build.turbines = {} build.turbines = {}
for i = 1, #self.turbines do for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session local turbine = self.turbines[i] ---@type unit_session
@ -716,6 +753,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end end
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 return build
end end
@ -730,6 +775,14 @@ function unit.new(reactor_id, num_boilers, num_turbines)
return status return status
end 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 -- get RTU statuses
---@nodiscard ---@nodiscard
function public.get_rtu_statuses() function public.get_rtu_statuses()
@ -738,40 +791,54 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- status of boilers (including tanks) -- status of boilers (including tanks)
status.boilers = {} status.boilers = {}
for i = 1, #self.boilers do for i = 1, #self.boilers do
local boiler = self.boilers[i] ---@type unit_session local boiler = self.boilers[i] ---@type unit_session
status.boilers[boiler.get_device_idx()] = { local db = boiler.get_db() ---@type boilerv_session_db
boiler.is_faulted(), status.boilers[boiler.get_device_idx()] = { boiler.is_faulted(), db.formed, db.state, db.tanks }
boiler.get_db().formed,
boiler.get_db().state,
boiler.get_db().tanks
}
end end
-- status of turbines (including tanks) -- status of turbines (including tanks)
status.turbines = {} status.turbines = {}
for i = 1, #self.turbines do for i = 1, #self.turbines do
local turbine = self.turbines[i] ---@type unit_session local turbine = self.turbines[i] ---@type unit_session
status.turbines[turbine.get_device_idx()] = { local db = turbine.get_db() ---@type turbinev_session_db
turbine.is_faulted(), status.turbines[turbine.get_device_idx()] = { turbine.is_faulted(), db.formed, db.state, db.tanks }
turbine.get_db().formed,
turbine.get_db().state,
turbine.get_db().tanks
}
end 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) -- radiation monitors (environment detectors)
status.rad_mon = {} status.rad_mon = {}
for i = 1, #self.envd do for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
status.rad_mon[envd.get_device_idx()] = { status.rad_mon[envd.get_device_idx()] = { envd.is_faulted(), envd.get_db().radiation }
envd.is_faulted(),
envd.get_db().radiation
}
end end
return status return status
end 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 -- get the annunciator status
---@nodiscard ---@nodiscard
function public.get_annunciator() return self.db.annunciator end 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 -- get unit state
---@nodiscard ---@nodiscard
function public.get_state() 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 end
-- get the reactor ID -- get the reactor ID