Merge pull request #542 from MikaylaFischler/devel

2024.08.25 Release
This commit is contained in:
Mikayla 2024-08-25 22:50:18 -04:00 committed by GitHub
commit 07406ca5fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1911 additions and 1230 deletions

View File

@ -15,15 +15,63 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]-- ]]--
local function println(message) print(tostring(message)) end local CCMSI_VERSION = "v1.17"
local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.16"
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/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/" local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
---@diagnostic disable-next-line: undefined-global
local _is_pkt_env = pocket -- luacheck: ignore pocket
local function println(msg) print(tostring(msg)) end
-- stripped down & modified copy of log.dmesg
local function print(msg)
msg = tostring(msg)
local cur_x, cur_y = term.getCursorPos()
local out_w, out_h = term.getSize()
-- jump to next line if needed
if cur_x == out_w then
cur_x = 1
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else
term.setCursorPos(1, cur_y + 1)
end
end
-- wrap
local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
while remaining do
local line = string.sub(msg, s_start, s_end)
if line == "" then
remaining = false
else
lines[ln] = line
s_start = s_end + 1
s_end = s_end + out_w
ln = ln + 1
end
end
-- print
for i = 1, #lines do
cur_x, cur_y = term.getCursorPos()
if i > 1 and cur_x > 1 then
if cur_y == out_h then
term.scroll(1)
term.setCursorPos(1, cur_y)
else term.setCursorPos(1, cur_y + 1) end
end
term.write(lines[i])
end
end
local opts = { ... } local opts = { ... }
local mode, app, target local mode, app, target
local install_manifest = manifest_path.."main/install_manifest.json" local install_manifest = manifest_path.."main/install_manifest.json"
@ -219,10 +267,27 @@ end
-- get and validate command line options -- get and validate command line options
println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") if _is_pkt_env then println("- SCADA Installer "..CCMSI_VERSION.." -")
else println("-- CC Mekanism SCADA Installer "..CCMSI_VERSION.." --") end
if #opts == 0 or opts[1] == "help" then if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <branch>") println("usage: ccmsi <mode> <app> <branch>")
if _is_pkt_env then
yellow();println("<mode>");lgray()
println(" check - check latest")
println(" install - fresh install")
println(" update - update app")
println(" uninstall - remove app")
yellow();println("<app>");lgray()
println(" reactor-plc")
println(" rtu")
println(" supervisor")
println(" coordinator")
println(" pocket")
println(" installer (update only)")
yellow();println("<branch>");lgray();
println(" main (default) | devel");white()
else
println("<mode>") println("<mode>")
lgray() lgray()
println(" check - check latest versions available") println(" check - check latest versions available")
@ -241,6 +306,7 @@ if #opts == 0 or opts[1] == "help" then
println(" installer - ccmsi installer (update only)") println(" installer - ccmsi installer (update only)")
white();println("<branch>") white();println("<branch>")
lgray();println(" main (default) | devel");white() lgray();println(" main (default) | devel");white()
end
return return
else else
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" }) mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
@ -286,20 +352,22 @@ if mode == "check" then
-- list all versions -- list all versions
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
term.setTextColor(colors.purple) term.setTextColor(colors.purple)
print(string.format("%-14s", "["..key.."]")) local tag = string.format("%-14s", "["..key.."]")
if not _is_pkt_env then print(tag) end
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
if _is_pkt_env then println(tag) end
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(" (")
cyan();print(value);white();println(" available)") cyan();print(value);white();println(" available)")
else green();println(" (up to date)") end else green();println(" (up to date)") end
else elseif not _is_pkt_env then
lgray();print("not installed");white();print(" (latest ") lgray();print("not installed");white();print(" (latest ")
cyan();print(value);white();println(")") cyan();print(value);white();println(")")
end end
end end
if manifest.versions.installer ~= local_manifest.versions.installer then if manifest.versions.installer ~= local_manifest.versions.installer and not _is_pkt_env then
yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white() yellow();println("\nA different version of the installer is available, it is recommended to update (use 'ccmsi update installer').");white()
end end
elseif mode == "install" or mode == "update" then elseif mode == "install" or mode == "update" then

View File

@ -959,7 +959,10 @@ local function config_view(display)
end end
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/coordinator.settings") then if settings.save("/coordinator.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)

View File

@ -520,7 +520,7 @@ function coordinator.comms(version, nic, sv_watchdog)
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() + 1 self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv_r_seq_num ~= packet.scada_frame.seq_num() then elseif self.sv_r_seq_num ~= 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: next = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return false 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?")

View File

@ -89,6 +89,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
num_units = conf.num_units, num_units = conf.num_units,
tank_mode = conf.cooling.fac_tank_mode, tank_mode = conf.cooling.fac_tank_mode,
tank_defs = conf.cooling.fac_tank_defs, tank_defs = conf.cooling.fac_tank_defs,
tank_list = conf.cooling.fac_tank_list,
all_sys_ok = false, all_sys_ok = false,
rtu_count = 0, rtu_count = 0,
@ -143,92 +144,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
table.insert(io.facility.sps_ps_tbl, psil.create()) table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {}) table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables -- create facility tank tables
for i = 1, #io.facility.tank_list do for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then if io.facility.tank_list[i] == 2 then

View File

@ -106,7 +106,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) log.warning(log_header .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return return
else else
self.r_seq_num = pkt.scada_frame.seq_num() + 1 self.r_seq_num = pkt.scada_frame.seq_num() + 1
@ -186,6 +186,10 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_header .. "terminated session due to an unexpected ESTABLISH packet")
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end

View File

@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads") local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.5.2" local COORDINATOR_VERSION = "v1.5.6"
local CHUNK_LOAD_DELAY_S = 30.0 local CHUNK_LOAD_DELAY_S = 30.0

View File

@ -132,8 +132,8 @@ local function init(panel, num_units)
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg} local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) 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) comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

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

View File

@ -2,6 +2,7 @@
-- Generic Graphics Element -- Generic Graphics Element
-- --
-- local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
@ -503,7 +504,10 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
if args.parent ~= nil then if args.parent ~= nil then
-- remove self from parent -- remove self from parent
-- log.debug("removing " .. self.id .. " from parent")
args.parent.__remove_child(self.id) args.parent.__remove_child(self.id)
else
-- log.debug("no parent for " .. self.id .. " on delete attempt")
end end
end end

View File

@ -1,5 +1,6 @@
-- Scroll-able List Box Display Graphics Element -- Scroll-able List Box Display Graphics Element
-- local log = require("scada-common.log")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local core = require("graphics.core") local core = require("graphics.core")
@ -152,6 +153,7 @@ local function listbox(args)
next_y = next_y + item.h + item_pad next_y = next_y + item.h + item_pad
item.e.reposition(1, item.y) item.e.reposition(1, item.y)
item.e.show() item.e.show()
-- log.debug("iterated " .. item.e.get_id())
end end
content_height = next_y content_height = next_y
@ -210,6 +212,7 @@ local function listbox(args)
---@param child graphics_element child element ---@param child graphics_element child element
function e.on_added(id, child) function e.on_added(id, child)
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() }) table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
-- log.debug("added child " .. id .. " into slot " .. #list)
update_positions() update_positions()
end end
@ -219,10 +222,12 @@ local function listbox(args)
for idx, elem in ipairs(list) do for idx, elem in ipairs(list) do
if elem.id == id then if elem.id == id then
table.remove(list, idx) table.remove(list, idx)
-- log.debug("removed child " .. id .. " from slot " .. idx)
update_positions() update_positions()
return return
end end
end end
-- log.debug("failed to remove child " .. id)
end end
-- handle focus -- handle focus

View File

@ -57,6 +57,9 @@ local function textbox(args)
for i = 1, #lines do for i = 1, #lines do
if i > e.frame.h then break end if i > e.frame.h then break end
-- trim leading/trailing whitespace
lines[i] = util.trim(lines[i])
local len = string.len(lines[i]) local len = string.len(lines[i])
-- use cursor position to align this line -- use cursor position to align this line

View File

@ -379,7 +379,10 @@ local function config_view(display)
end end
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/pocket.settings") then if settings.save("/pocket.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)

View File

@ -376,6 +376,12 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
end end
end end
-- show the reason the supervisor connection isn't linking
function iocontrol.report_svr_link_error(msg) io.ps.publish("svr_link_msg", msg) end
-- show the reason the coordinator api connection isn't linking
function iocontrol.report_crd_link_error(msg) io.ps.publish("api_link_msg", msg) end
-- determine supervisor connection quality (trip time) -- determine supervisor connection quality (trip time)
---@param trip_time integer ---@param trip_time integer
function iocontrol.report_svr_tt(trip_time) function iocontrol.report_svr_tt(trip_time)

View File

@ -610,7 +610,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.api.r_seq_num == nil then if self.api.r_seq_num == nil then
self.api.r_seq_num = packet.scada_frame.seq_num() + 1 self.api.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then elseif self.api.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (API): next = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.api.linked and (src_addr ~= self.api.addr) then elseif self.api.linked and (src_addr ~= self.api.addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr ..
@ -686,6 +686,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
self.api.linked = true self.api.linked = true
self.api.addr = src_addr self.api.addr = src_addr
iocontrol.report_crd_link_error("")
if self.sv.linked then if self.sv.linked then
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr) iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
else else
@ -697,24 +699,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
else else
log.debug("received coordinator establish allow without facility configuration") log.debug("received coordinator establish allow without facility configuration")
end end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator comms version mismatch")
end
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
if self.api.last_est_ack ~= est_ack then
log.info("coordinator api version mismatch")
end
else else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported") if self.api.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("coordinator connection denied")
iocontrol.report_crd_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("coordinator connection denied due to collision")
iocontrol.report_crd_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("coordinator comms version mismatch")
iocontrol.report_crd_link_error("comms version mismatch")
elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then
log.info("coordinator api version mismatch")
iocontrol.report_crd_link_error("API version mismatch")
else
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
iocontrol.report_crd_link_error("unknown reply")
end
end
-- unlink
self.api.addr = comms.BROADCAST
self.api.linked = false
end end
self.api.last_est_ack = est_ack self.api.last_est_ack = est_ack
@ -730,7 +737,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
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() + 1 self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then elseif self.sv.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order (SVR): next = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
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 (SVR expected " .. self.sv.addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr ..
@ -826,25 +833,33 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
self.sv.linked = true self.sv.linked = true
self.sv.addr = src_addr self.sv.addr = src_addr
iocontrol.report_svr_link_error("")
if self.api.linked then if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil) iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
else else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil) iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
end end
elseif est_ack == ESTABLISH_ACK.DENY then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.sv.last_est_ack ~= est_ack then
log.info("supervisor comms version mismatch")
end
else else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported") if self.sv.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
log.info("supervisor connection denied")
iocontrol.report_svr_link_error("denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
log.info("supervisor connection denied due to collision")
iocontrol.report_svr_link_error("collision")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
log.info("supervisor comms version mismatch")
iocontrol.report_svr_link_error("comms version mismatch")
else
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
iocontrol.report_svr_link_error("unknown reply")
end
end
-- unlink
self.sv.addr = comms.BROADCAST
self.sv.linked = false
end end
self.sv.last_est_ack = est_ack self.sv.last_est_ack = est_ack

View File

@ -20,7 +20,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local threads = require("pocket.threads") local threads = require("pocket.threads")
local POCKET_VERSION = "v0.11.4-alpha" local POCKET_VERSION = "v0.11.8-alpha"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -144,7 +144,7 @@ local function new_view(root)
for idx = 1, #s_results[tier] do for idx = 1, #s_results[tier] do
local entry = s_results[tier][idx] local entry = s_results[tier][idx]
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)} TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)}
PushButton{parent=search_results,text=entry[2],alignment=ALIGN.LEFT,fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]} PushButton{parent=search_results,text=entry[2],fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=entry[4]}
empty = false empty = false
end end

View File

@ -63,25 +63,25 @@ local function create_pages(root)
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",fg_bg=label}
---@diagnostic disable-next-line: undefined-field ---@diagnostic disable-next-line: undefined-field
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel),alignment=ALIGN.LEFT} TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel)}
nt_div.line_break() nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Supervisor Address",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=nt_div,x=2,text="Supervisor Address",fg_bg=label}
local sv = TextBox{parent=nt_div,x=2,text="",alignment=ALIGN.LEFT} local sv = TextBox{parent=nt_div,x=2,text=""}
nt_div.line_break() nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Coordinator Address",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=nt_div,x=2,text="Coordinator Address",fg_bg=label}
local coord = TextBox{parent=nt_div,x=2,text="",alignment=ALIGN.LEFT} local coord = TextBox{parent=nt_div,x=2,text=""}
sv.register(db.ps, "sv_addr", sv.set_value) sv.register(db.ps, "sv_addr", sv.set_value)
coord.register(db.ps, "api_addr", coord.set_value) coord.register(db.ps, "api_addr", coord.set_value)
nt_div.line_break() nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Message Authentication",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=nt_div,x=2,text="Message Authentication",fg_bg=label}
local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None") local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None")
TextBox{parent=nt_div,x=2,text=auth,alignment=ALIGN.LEFT} TextBox{parent=nt_div,x=2,text=auth}
--#endregion --#endregion
@ -96,28 +96,28 @@ local function create_pages(root)
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18} local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
TextBox{parent=fw_list,x=2,text="Pocket Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=fw_list,x=2,text="Pocket Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=db.version,alignment=ALIGN.LEFT} TextBox{parent=fw_list,x=2,text=db.version}
fw_list.line_break() fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Comms Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=fw_list,x=2,text="Comms Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=comms.version,alignment=ALIGN.LEFT} TextBox{parent=fw_list,x=2,text=comms.version}
fw_list.line_break() fw_list.line_break()
TextBox{parent=fw_list,x=2,text="API Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=fw_list,x=2,text="API Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=comms.api_version,alignment=ALIGN.LEFT} TextBox{parent=fw_list,x=2,text=comms.api_version}
fw_list.line_break() fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Common Lib Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=fw_list,x=2,text="Common Lib Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=util.version,alignment=ALIGN.LEFT} TextBox{parent=fw_list,x=2,text=util.version}
fw_list.line_break() fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Graphics Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=fw_list,x=2,text="Graphics Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=core.version,alignment=ALIGN.LEFT} TextBox{parent=fw_list,x=2,text=core.version}
fw_list.line_break() fw_list.line_break()
TextBox{parent=fw_list,x=2,text="Lockbox Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=fw_list,x=2,text="Lockbox Version",fg_bg=label}
TextBox{parent=fw_list,x=2,text=lockbox.version,alignment=ALIGN.LEFT} TextBox{parent=fw_list,x=2,text=lockbox.version}
--#endregion --#endregion
@ -129,12 +129,12 @@ local function create_pages(root)
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
hw_div.line_break() hw_div.line_break()
TextBox{parent=hw_div,x=2,text="Lua Version",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=hw_div,x=2,text="Lua Version",fg_bg=label}
TextBox{parent=hw_div,x=2,text=_VERSION,alignment=ALIGN.LEFT} TextBox{parent=hw_div,x=2,text=_VERSION}
hw_div.line_break() hw_div.line_break()
TextBox{parent=hw_div,x=2,text="Environment",alignment=ALIGN.LEFT,fg_bg=label} TextBox{parent=hw_div,x=2,text="Environment",fg_bg=label}
TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT} TextBox{parent=hw_div,x=2,text=_HOST,height=6}
--#endregion --#endregion

View File

@ -2,6 +2,8 @@
-- Connection Waiting Spinner -- Connection Waiting Spinner
-- --
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")
local core = require("graphics.core") local core = require("graphics.core")
@ -23,16 +25,20 @@ local function init(parent, y, is_api)
local root = Div{parent=parent,x=1,y=1} local root = Div{parent=parent,x=1,y=1}
-- bounding box div -- bounding box div
local box = Div{parent=root,x=1,y=y,height=6} local box = Div{parent=root,x=1,y=y,height=12}
local waiting_x = math.floor(parent.get_width() / 2) - 1 local waiting_x = math.floor(parent.get_width() / 2) - 1
local msg = TextBox{parent=box,x=3,y=11,width=box.get_width()-4,height=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.red,style.root.bkg)}
if is_api then if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
TextBox{parent=box,text="Connecting to API",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)} TextBox{parent=box,y=5,text="Connecting to API",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
msg.register(iocontrol.get_db().ps, "api_link_msg", msg.set_value)
else else
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)} WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.green,style.root.bkg)}
TextBox{parent=box,text="Connecting to Supervisor",alignment=ALIGN.CENTER,y=5,fg_bg=cpair(colors.white,style.root.bkg)} TextBox{parent=box,y=5,text="Connecting to Supervisor",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,style.root.bkg)}
msg.register(iocontrol.get_db().ps, "svr_link_msg", msg.set_value)
end end
return root return root

View File

@ -42,7 +42,7 @@ local function init(main)
local db = iocontrol.get_db() local db = iocontrol.get_db()
-- window header message and connection status -- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",alignment=ALIGN.LEFT,fg_bg=style.header} TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)} local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)} local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}

View File

@ -55,7 +55,7 @@ return function (data, base_page, title, items, scroll_height)
doc_map[item.key] = view doc_map[item.key] = view
table.insert(search_db, { string.lower(item.name), item.name, title, view }) table.insert(search_db, { string.lower(item.name), item.name, title, view })
PushButton{parent=name_list,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view} PushButton{parent=name_list,text=item.name,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
if i % 12 == 0 then util.nop() end if i % 12 == 0 then util.nop() end
end end

View File

@ -34,9 +34,20 @@ local self = {
settings = nil, ---@type plc_config settings = nil, ---@type plc_config
run_test_btn = nil, ---@type graphics_element run_test_btn = nil, ---@type graphics_element
sc_log = nil, ---@type graphics_element
self_check_msg = nil ---@type function self_check_msg = nil ---@type function
} }
-- report successful completion of the check
local function check_complete()
TextBox{parent=self.sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)}
TextBox{parent=self.sc_log,text=""}
local more = Div{parent=self.sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)}
TextBox{parent=more,text="if you still have a problem:"}
TextBox{parent=more,text="- check the wiki on GitHub"}
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
end
-- send a management packet to the supervisor -- send a management packet to the supervisor
---@param msg_type MGMT_TYPE ---@param msg_type MGMT_TYPE
---@param msg table ---@param msg table
@ -67,6 +78,7 @@ local function handle_packet(packet)
self.self_check_msg(nil, true, "") self.self_check_msg(nil, true, "")
self.sv_addr = packet.scada_frame.src_addr() self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {}) send_sv(MGMT_TYPE.CLOSE, {})
if self.self_check_pass then check_complete() end
elseif est_ack == ESTABLISH_ACK.DENY then elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied" error_msg = "error: supervisor connection denied"
elseif est_ack == ESTABLISH_ACK.COLLISION then elseif est_ack == ESTABLISH_ACK.COLLISION then
@ -100,11 +112,10 @@ local function handle_timeout()
end end
-- execute the self-check -- execute the self-check
---@param sc_log graphics_element local function self_check()
local function self_check(sc_log)
self.run_test_btn.disable() self.run_test_btn.disable()
sc_log.remove_all() self.sc_log.remove_all()
ppm.mount_all() ppm.mount_all()
self.self_check_pass = true self.self_check_pass = true
@ -143,27 +154,18 @@ local function self_check(sc_log)
tcd.dispatch_unique(8, handle_timeout) tcd.dispatch_unique(8, handle_timeout)
else else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable() self.run_test_btn.enable()
end end
if self.self_check_pass then
TextBox{parent=sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)}
TextBox{parent=sc_log,text=""}
local more = Div{parent=sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)}
TextBox{parent=more,text="if you still have a problem:"}
TextBox{parent=more,text="- check the wiki on GitHub"}
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
end
end end
-- exit self check back home -- exit self check back home
---@param sc_log graphics_element
---@param main_pane graphics_element ---@param main_pane graphics_element
local function exit_self_check(sc_log, main_pane) local function exit_self_check(main_pane)
tcd.abort(handle_timeout) tcd.abort(handle_timeout)
self.net_listen = false self.net_listen = false
self.run_test_btn.enable() self.run_test_btn.enable()
sc_log.remove_all() self.sc_log.remove_all()
main_pane.set_value(1) main_pane.set_value(1)
end end
@ -187,13 +189,13 @@ function check.create(main_pane, settings_cfg, check_sys, style)
TextBox{parent=check_sys,x=1,y=2,text=" Reactor PLC Self-Check",fg_bg=bw_fg_bg} TextBox{parent=check_sys,x=1,y=2,text=" Reactor PLC Self-Check",fg_bg=bw_fg_bg}
local sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local last_check = { nil, nil } local last_check = { nil, nil }
function self.self_check_msg(msg, success, fail_msg) function self.self_check_msg(msg, success, fail_msg)
if type(msg) == "string" then if type(msg) == "string" then
last_check[1] = Div{parent=sc_log,height=1} last_check[1] = Div{parent=self.sc_log,height=1}
local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg} local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg}
last_check[2] = e.get_x()+string.len(msg) last_check[2] = e.get_x()+string.len(msg)
end end
@ -202,7 +204,7 @@ function check.create(main_pane, settings_cfg, check_sys, style)
TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))} TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))}
if not success then if not success then
local fail = Div{parent=sc_log,height=#util.strwrap(fail_msg, 46)} local fail = Div{parent=self.sc_log,height=#util.strwrap(fail_msg, 46)}
TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)}
end end
@ -210,8 +212,8 @@ function check.create(main_pane, settings_cfg, check_sys, style)
end end
end end
PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(sc_log,main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check(sc_log)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end end
-- handle incoming modem messages -- handle incoming modem messages

View File

@ -450,7 +450,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
end end
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/reactor-plc.settings") then if settings.save("/reactor-plc.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)

View File

@ -148,8 +148,8 @@ local function init(panel)
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg} local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

@ -833,7 +833,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() + 1 self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif self.linked and (src_addr ~= self.sv_addr) then elseif self.linked and (src_addr ~= self.sv_addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.8.2" local R_PLC_VERSION = "v1.8.6"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -650,8 +650,11 @@ local function config_view(display)
---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections ---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections
local function save_and_continue(exclude_conns) local function save_and_continue(exclude_conns)
for k, v in pairs(tmp_cfg) do for _, field in ipairs(fields) do
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then settings.set(k, v) end local k, v = field[1], tmp_cfg[field[1]]
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then
if v == nil then settings.unset(k) else settings.set(k, v) end
end
end end
-- always set these if missing -- always set these if missing

View File

@ -109,8 +109,8 @@ local function init(panel, units)
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg} local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)

View File

@ -444,7 +444,7 @@ function rtu.comms(version, nic, conn_watchdog)
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() + 1 self.r_seq_num = packet.scada_frame.seq_num() + 1
elseif self.r_seq_num ~= packet.scada_frame.seq_num() then elseif self.r_seq_num ~= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) log.warning("sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return return
elseif rtu_state.linked and (src_addr ~= self.sv_addr) then elseif rtu_state.linked and (src_addr ~= self.sv_addr) then
log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr ..

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.10.3" local RTU_VERSION = "v1.10.6"
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

View File

@ -5,7 +5,7 @@
---@class types ---@class types
local types = {} local types = {}
-- CLASSES -- --#region CLASSES
---@class tank_fluid ---@class tank_fluid
---@field name fluid ---@field name fluid
@ -67,12 +67,13 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@field reactor integer ---@field reactor integer
---@field rsio table|nil ---@field rsio table|nil
--#endregion
-- ALIASES -- -- ALIASES --
---@alias color integer ---@alias color integer
-- ENUMERATION TYPES -- --#region ENUMERATION TYPES
--#region
---@enum TEMP_SCALE ---@enum TEMP_SCALE
types.TEMP_SCALE = { types.TEMP_SCALE = {
@ -169,6 +170,15 @@ function types.rtu_type_to_string(utype)
end end
end end
---@enum RTU_ID_FAIL
types.RTU_ID_FAIL = {
OK = 0,
OUT_OF_RANGE = 1,
DUPLICATE = 2,
MAX_DEVICES = 3,
MISSING = 4
}
---@enum TRI_FAIL ---@enum TRI_FAIL
types.TRI_FAIL = { types.TRI_FAIL = {
OK = 1, OK = 1,
@ -290,8 +300,7 @@ types.ALARM_STATE_NAMES = {
--#endregion --#endregion
-- STRING TYPES -- --#region STRING TYPES
--#region
---@alias side ---@alias side
---|"top" ---|"top"
@ -405,8 +414,7 @@ types.DUMPING_MODE = {
--#endregion --#endregion
-- MODBUS -- --#region MODBUS
--#region
-- MODBUS function codes -- MODBUS function codes
---@enum MODBUS_FCODE ---@enum MODBUS_FCODE

View File

@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.4.2" util.version = "1.4.3"
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50
@ -110,6 +110,15 @@ function util.pad(str, n)
return t_concat{util.spaces(lpad), str, util.spaces(rpad)} return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
end end
-- trim leading and trailing whitespace
---@nodiscard
---@param s string text
---@return string
function util.trim(s)
local str = s:gsub("^%s*(.-)%s*$", "%1")
return str
end
-- wrap a string into a table of lines -- wrap a string into a table of lines
---@nodiscard ---@nodiscard
---@param str string ---@param str string

View File

@ -907,7 +907,10 @@ local function config_view(display)
end end
local function save_and_continue() local function save_and_continue()
for k, v in pairs(tmp_cfg) do settings.set(k, v) end for _, field in ipairs(fields) do
local k, v = field[1], tmp_cfg[field[1]]
if v == nil then settings.unset(k) else settings.set(k, v) end
end
if settings.save("/supervisor.settings") then if settings.save("/supervisor.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,832 @@
local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes")
local TONE = audio.TONE
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local CONTAINER_MODE = types.CONTAINER_MODE
local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local IO = rsio.IO
local ALARM_LIMS = const.ALARM_LIMITS
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
-- 7.14 kJ per blade for 1 mB of fissile fuel<br>
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
local POWER_PER_BLADE = util.joules_to_fe_rf(7140)
local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000
local CHARGE_Kp = 0.15
local CHARGE_Ki = 0.0
local CHARGE_Kd = 0.6
local RATE_Kp = 2.45
local RATE_Ki = 0.4825
local RATE_Kd = -1.0
local self = nil ---@type _facility_self
local next_mode = 0
local charge_update = 0
local rate_update = 0
---@class facility_update_extension
local update = {}
--#region PRIVATE FUNCTIONS
-- check if all auto-controlled units completed ramping
---@nodiscard
local function all_units_ramped()
local all_ramped = true
for i = 1, #self.prio_defs do
local units = self.prio_defs[i]
for u = 1, #units do
all_ramped = all_ramped and units[u].auto_ramp_complete()
end
end
return all_ramped
end
-- split a burn rate among the reactors
---@param burn_rate number burn rate assignment
---@param ramp boolean true to ramp, false to set right away
---@param abort_on_fault boolean? true to exit if one device has an effective burn rate different than its limit
---@return integer unallocated_br100, boolean? aborted
local function allocate_burn_rate(burn_rate, ramp, abort_on_fault)
local unallocated = math.floor(burn_rate * 100)
-- go through all priority groups
for i = 1, #self.prio_defs do
local units = self.prio_defs[i]
if #units > 0 then
local split = math.floor(unallocated / #units)
local splits = {}
for u = 1, #units do splits[u] = split end
splits[#units] = splits[#units] + (unallocated % #units)
-- go through all reactor units in this group
for id = 1, #units do
local u = units[id] ---@type reactor_unit
local ctl = u.get_control_inf()
local lim_br100 = u.auto_get_effective_limit()
if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then
-- effective limit differs from set limit, unit is degraded
return unallocated, true
end
local last = ctl.br100
if splits[id] <= lim_br100 then
ctl.br100 = splits[id]
else
ctl.br100 = lim_br100
if id < #units then
local remaining = #units - id
split = math.floor(unallocated / remaining)
for x = (id + 1), #units do splits[x] = split end
splits[#units] = splits[#units] + (unallocated % remaining)
end
end
unallocated = math.max(0, unallocated - ctl.br100)
if last ~= ctl.br100 then u.auto_commit_br100(ramp) end
end
end
end
return unallocated, false
end
-- set idle state of all assigned reactors
---@param idle boolean idle state
local function set_idling(idle)
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end
end
end
--#endregion
--#region PUBLIC FUNCTIONS
-- automatic control pre-update logic
function update.pre_auto()
-- unlink RTU sessions if they are closed
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
-- check if test routines are allowed right now
self.allow_testing = true
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
self.allow_testing = self.allow_testing and u.is_safe_idle()
end
-- current state for process control
charge_update = 0
rate_update = 0
-- calculate moving averages for induction matrix
if self.induction[1] ~= nil then
local matrix = self.induction[1] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
local build_update = db.build.last_update
rate_update = db.state.last_update
charge_update = db.tanks.last_update
local has_data = build_update > 0 and rate_update > 0 and charge_update > 0
if matrix.is_faulted() then
-- a fault occured, cannot reliably update stats
has_data = false
self.im_stat_init = false
self.imtx_faulted_times = { build_update, rate_update, charge_update }
elseif not self.im_stat_init then
-- prevent operation with partially invalid data
-- all fields must have updated since the last fault
has_data = self.imtx_faulted_times[1] < build_update and
self.imtx_faulted_times[2] < rate_update and
self.imtx_faulted_times[3] < charge_update
end
if has_data then
local energy = util.joules_to_fe_rf(db.tanks.energy)
local input = util.joules_to_fe_rf(db.state.last_input)
local output = util.joules_to_fe_rf(db.state.last_output)
if self.im_stat_init then
self.avg_charge.record(energy, charge_update)
self.avg_inflow.record(input, rate_update)
self.avg_outflow.record(output, rate_update)
if charge_update ~= self.imtx_last_charge_t then
local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t)
self.imtx_last_charge = energy
self.imtx_last_charge_t = charge_update
-- if the capacity changed, toss out existing data
if db.build.max_energy ~= self.imtx_last_capacity then
self.imtx_last_capacity = db.build.max_energy
self.avg_net.reset()
else
self.avg_net.record(delta, charge_update)
end
end
else
self.im_stat_init = true
self.avg_charge.reset(energy)
self.avg_inflow.reset(input)
self.avg_outflow.reset(output)
self.avg_net.reset()
self.imtx_last_capacity = db.build.max_energy
self.imtx_last_charge = energy
self.imtx_last_charge_t = charge_update
end
else
-- prevent use by control systems
rate_update = 0
charge_update = 0
end
else
self.im_stat_init = false
end
self.all_sys_ok = true
for i = 1, #self.units do
self.all_sys_ok = self.all_sys_ok and not self.units[i].get_control_inf().degraded
end
end
-- run auto control
---@param ExtChargeIdling boolean ExtChargeIdling config field
function update.auto_control(ExtChargeIdling)
local AUTO_SCRAM = self.types.AUTO_SCRAM
local START_STATUS = self.types.START_STATUS
local avg_charge = self.avg_charge.compute()
local avg_inflow = self.avg_inflow.compute()
local avg_outflow = self.avg_outflow.compute()
local now = os.clock()
local state_changed = self.mode ~= self.last_mode
next_mode = self.mode
-- once auto control is started, sort the priority sublists by limits
if state_changed then
self.saturated = false
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK
if (self.mode ~= PROCESS.MATRIX_FAULT_IDLE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
-- auto clear ASCRAM
self.ascram = false
self.ascram_reason = AUTO_SCRAM.NONE
end
local blade_count = nil
self.max_burn_combined = 0.0
for i = 1, #self.prio_defs do
table.sort(self.prio_defs[i],
---@param a reactor_unit
---@param b reactor_unit
function (a, b) return a.get_control_inf().lim_br100 < b.get_control_inf().lim_br100 end
)
for _, u in pairs(self.prio_defs[i]) do
local u_blade_count = u.get_control_inf().blade_count
if blade_count == nil then
blade_count = u_blade_count
elseif (u_blade_count ~= blade_count) and (self.mode == PROCESS.GEN_RATE) then
log.warning("FAC: cannot start GEN_RATE process with inconsistent unit blade counts")
next_mode = PROCESS.INACTIVE
self.start_fail = START_STATUS.BLADE_MISMATCH
end
if self.start_fail == START_STATUS.OK then u.auto_engage() end
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0)
end
end
log.debug(util.c("FAC: computed a max combined burn rate of ", self.max_burn_combined, "mB/t"))
if blade_count == nil then
-- no units
log.warning("FAC: cannot start process control with 0 units assigned")
next_mode = PROCESS.INACTIVE
self.start_fail = START_STATUS.NO_UNITS
else
self.charge_conversion = blade_count * POWER_PER_BLADE
end
elseif self.mode == PROCESS.INACTIVE then
for i = 1, #self.prio_defs do
-- disable reactors and disengage auto control
for _, u in pairs(self.prio_defs[i]) do
u.disable()
u.auto_set_idle(false)
u.auto_disengage()
end
end
log.info("FAC: disengaging auto control (now inactive)")
end
self.initial_ramp = true
self.waiting_on_ramp = false
self.waiting_on_stable = false
else
self.initial_ramp = false
end
-- update unit ready state
local assign_count = 0
self.units_ready = true
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
assign_count = assign_count + 1
self.units_ready = self.units_ready and u.get_control_inf().ready
end
end
-- perform mode-specific operations
if self.mode == PROCESS.INACTIVE then
if not self.units_ready then
self.status_text = { "NOT READY", "assigned units not ready" }
else
-- clear ASCRAM once ready
self.ascram = false
self.ascram_reason = AUTO_SCRAM.NONE
if self.start_fail == START_STATUS.NO_UNITS and assign_count == 0 then
self.status_text = { "START FAILED", "no units were assigned" }
elseif self.start_fail == START_STATUS.BLADE_MISMATCH then
self.status_text = { "START FAILED", "turbine blade count mismatch" }
else
self.status_text = { "IDLE", "control disengaged" }
end
end
elseif self.mode == PROCESS.MAX_BURN then
-- run units at their limits
if state_changed then
self.time_start = now
self.saturated = true
self.status_text = { "MONITORED MODE", "running reactors at limit" }
log.info("FAC: MAX_BURN process mode started")
end
allocate_burn_rate(self.max_burn_combined, true)
elseif self.mode == PROCESS.BURN_RATE then
-- a total aggregate burn rate
if state_changed then
self.time_start = now
self.status_text = { "BURN RATE MODE", "running" }
log.info("FAC: BURN_RATE process mode started")
end
local unallocated = allocate_burn_rate(self.burn_target, true)
self.saturated = self.burn_target == self.max_burn_combined or unallocated > 0
elseif self.mode == PROCESS.CHARGE then
-- target a level of charge
if state_changed then
self.time_start = now
self.last_time = now
self.last_error = 0
self.accumulator = 0
-- enabling idling on all assigned units
set_idling(true)
self.status_text = { "CHARGE MODE", "running control loop" }
log.info("FAC: CHARGE mode starting PID control")
elseif self.last_update < charge_update then
-- convert to kFE to make constants not microscopic
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
-- stop accumulator when saturated to avoid windup
if not self.saturated then
self.accumulator = self.accumulator + (error * (now - self.last_time))
end
-- local runtime = now - self.time_start
local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time)
local P = CHARGE_Kp * error
local I = CHARGE_Ki * integral
local D = CHARGE_Kd * derivative
local output = P + I + D
-- clamp at range -> output clamped (out_c)
local out_c = math.max(0, math.min(output, self.max_burn_combined))
self.saturated = output ~= out_c
if not ExtChargeIdling then
-- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge
set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0)))
end
-- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
-- runtime, avg_charge, error, integral, output, out_c, P, I, D))
allocate_burn_rate(out_c, true)
self.last_time = now
self.last_error = error
end
self.last_update = charge_update
elseif self.mode == PROCESS.GEN_RATE then
-- target a rate of generation
if state_changed then
-- estimate an initial output
local output = self.gen_rate_setpoint / self.charge_conversion
local unallocated = allocate_burn_rate(output, true)
self.saturated = output >= self.max_burn_combined or unallocated > 0
self.waiting_on_ramp = true
self.status_text = { "GENERATION MODE", "starting up" }
log.info(util.c("FAC: GEN_RATE process mode initial ramp started (initial target is ", output, " mB/t)"))
elseif self.waiting_on_ramp then
if all_units_ramped() then
self.waiting_on_ramp = false
self.waiting_on_stable = true
self.time_start = now
self.status_text = { "GENERATION MODE", "holding ramped rate" }
log.info("FAC: GEN_RATE process mode initial ramp completed, holding for stablization time")
end
elseif self.waiting_on_stable then
if (now - self.time_start) > FLOW_STABILITY_DELAY_S then
self.waiting_on_stable = false
self.time_start = now
self.last_time = now
self.last_error = 0
self.accumulator = 0
self.status_text = { "GENERATION MODE", "running control loop" }
log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control")
end
elseif self.last_update < rate_update then
-- convert to MFE (in rounded kFE) to make constants not microscopic
local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000
-- stop accumulator when saturated to avoid windup
if not self.saturated then
self.accumulator = self.accumulator + (error * (now - self.last_time))
end
-- local runtime = now - self.time_start
local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time)
local P = RATE_Kp * error
local I = RATE_Ki * integral
local D = RATE_Kd * derivative
-- velocity (rate) (derivative of charge level => rate) feed forward
local FF = self.gen_rate_setpoint / self.charge_conversion
local output = P + I + D + FF
-- clamp at range -> output clamped (sp_c)
local out_c = math.max(0, math.min(output, self.max_burn_combined))
self.saturated = output ~= out_c
-- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
-- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
allocate_burn_rate(out_c, false)
self.last_time = now
self.last_error = error
end
self.last_update = rate_update
elseif self.mode == PROCESS.MATRIX_FAULT_IDLE then
-- exceeded charge, wait until condition clears
if self.ascram_reason == AUTO_SCRAM.NONE then
next_mode = self.return_mode
log.info("FAC: exiting matrix fault idle state due to fault resolution")
elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then
next_mode = PROCESS.SYSTEM_ALARM_IDLE
log.info("FAC: exiting matrix fault idle state due to critical unit alarm")
end
elseif self.mode == PROCESS.SYSTEM_ALARM_IDLE then
-- do nothing, wait for user to confirm (stop and reset)
elseif self.mode == PROCESS.GEN_RATE_FAULT_IDLE then
-- system faulted (degraded/not ready) while running generation rate mode
-- mode will need to be fully restarted once everything is OK to re-ramp to feed-forward
if self.units_ready then
log.info("FAC: system ready after faulting out of GEN_RATE process mode, switching back...")
next_mode = PROCESS.GEN_RATE
end
elseif self.mode ~= PROCESS.INACTIVE then
log.error(util.c("FAC: unsupported process mode ", self.mode, ", switching to inactive"))
next_mode = PROCESS.INACTIVE
end
end
-- update automatic safety logic
function update.auto_safety()
local AUTO_SCRAM = self.types.AUTO_SCRAM
local astatus = self.ascram_status
if self.induction[1] ~= nil then
local db = self.induction[1].get_db() ---@type imatrix_session_db
-- clear matrix disconnected
if astatus.matrix_dc then
astatus.matrix_dc = false
log.info("FAC: induction matrix reconnected, clearing ASCRAM condition")
end
-- check matrix fill too high
local was_fill = astatus.matrix_fill
astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
if was_fill and not astatus.matrix_fill then
log.info(util.c("FAC: charge state of induction matrix entered acceptable range <= ", ALARM_LIMS.CHARGE_RE_ENABLE * 100, "%"))
end
-- check for critical unit alarms
astatus.crit_alarm = false
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
if u.has_alarm_min_prio(PRIO.CRITICAL) then
astatus.crit_alarm = true
break
end
end
-- check for facility radiation
if #self.envd > 0 then
local max_rad = 0
for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session
local e_db = envd.get_db() ---@type envd_session_db
if e_db.radiation_raw > max_rad then max_rad = e_db.radiation_raw end
end
astatus.radiation = max_rad >= ALARM_LIMS.FAC_HIGH_RAD
else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
-- operator can restart the system or hit the stop/reset button
end
-- system not ready, will need to restart GEN_RATE mode
-- clears when we enter the fault waiting state
astatus.gen_fault = self.mode == PROCESS.GEN_RATE and not self.units_ready
else
astatus.matrix_dc = true
end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_dc or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault
if scram and not self.ascram then
-- SCRAM all units
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
u.auto_scram()
end
end
if astatus.crit_alarm then
-- highest priority alarm
next_mode = PROCESS.SYSTEM_ALARM_IDLE
self.ascram_reason = AUTO_SCRAM.CRIT_ALARM
self.status_text = { "AUTOMATIC SCRAM", "critical unit alarm tripped" }
log.info("FAC: automatic SCRAM due to critical unit alarm")
log.warning("FAC: emergency exit of process control due to critical unit alarm")
elseif astatus.radiation then
next_mode = PROCESS.SYSTEM_ALARM_IDLE
self.ascram_reason = AUTO_SCRAM.RADIATION
self.status_text = { "AUTOMATIC SCRAM", "facility radiation high" }
log.info("FAC: automatic SCRAM due to high facility radiation")
elseif astatus.matrix_dc then
next_mode = PROCESS.MATRIX_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.MATRIX_DC
self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" }
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
log.info("FAC: automatic SCRAM due to induction matrix disconnection")
elseif astatus.matrix_fill then
next_mode = PROCESS.MATRIX_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.MATRIX_FILL
self.status_text = { "AUTOMATIC SCRAM", "induction matrix fill high" }
if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.return_mode = self.mode end
log.info("FAC: automatic SCRAM due to induction matrix high charge")
elseif astatus.gen_fault then
-- lowest priority alarm
next_mode = PROCESS.GEN_RATE_FAULT_IDLE
self.ascram_reason = AUTO_SCRAM.GEN_FAULT
self.status_text = { "GENERATION MODE IDLE", "paused: system not ready" }
log.info("FAC: automatic SCRAM due to unit problem while in GEN_RATE mode, will resume once all units are ready")
end
end
self.ascram = scram
if not self.ascram then
self.ascram_reason = AUTO_SCRAM.NONE
-- reset PLC RPS trips if we should
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
u.auto_cond_rps_reset()
end
end
end
end
-- update last mode and set next mode
function update.post_auto()
self.last_mode = self.mode
self.mode = next_mode
end
-- update alarm audio control
function update.alarm_audio()
local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
-- reset tone states before re-evaluting
for i = 1, #self.tone_states do self.tone_states[i] = false end
if allow_test then
alarms = self.test_alarm_states
else
-- check all alarms for all units
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
for id, alarm in pairs(u.get_alarms()) do
alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED)
end
end
if not self.test_tone_reset then
-- clear testing alarms if we aren't using them
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
end
end
-- Evaluate Alarms --
-- containment breach is worst case CRITICAL alarm, this takes priority
if alarms[ALARM.ContainmentBreach] then
self.tone_states[TONE.T_1800Hz_Int_4Hz] = true
else
-- critical damage is highest priority CRITICAL level alarm
if alarms[ALARM.CriticalDamage] then
self.tone_states[TONE.T_660Hz_Int_125ms] = true
else
-- EMERGENCY level alarms + URGENT over temp
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
self.tone_states[TONE.T_544Hz_440Hz_Alt] = true
-- URGENT level turbine trip
elseif alarms[ALARM.TurbineTrip] then
self.tone_states[TONE.T_745Hz_Int_1Hz] = true
-- URGENT level reactor lost
elseif alarms[ALARM.ReactorLost] then
self.tone_states[TONE.T_340Hz_Int_2Hz] = true
-- TIMELY level alarms
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
self.tone_states[TONE.T_800Hz_Int] = true
end
end
-- check RPS transient URGENT level alarm
if alarms[ALARM.RPSTransient] then
self.tone_states[TONE.T_1000Hz_Int] = true
-- disable really painful audio combination
self.tone_states[TONE.T_340Hz_Int_2Hz] = false
end
end
-- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
self.tone_states[TONE.T_745Hz_Int_1Hz] = false
self.tone_states[TONE.T_800Hz_Int] = false
self.tone_states[TONE.T_1000Hz_Int] = false
end
-- add to tone states if testing is active
if allow_test then
for i = 1, #self.tone_states do
self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i]
end
self.test_tone_reset = false
else
if not self.test_tone_reset then
-- clear testing tones if we aren't using them
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
end
-- flag that tones were reset
self.test_tone_set = false
self.test_tone_reset = true
end
end
-- update facility redstone
---@param ack_all function acknowledge all alarms
function update.redstone(ack_all)
if #self.redstone > 0 then
-- handle facility SCRAM
if self.io_ctl.digital_read(IO.F_SCRAM) then
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
u.cond_scram()
end
end
-- handle facility ack
if self.io_ctl.digital_read(IO.F_ACK) then ack_all() end
-- update facility alarm outputs
local has_prio_alarm, has_any_alarm = false, false
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
if u.has_alarm_min_prio(PRIO.EMERGENCY) then
has_prio_alarm, has_any_alarm = true, true
break
elseif u.has_alarm_min_prio(PRIO.TIMELY) then
has_any_alarm = true
end
end
self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm)
self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
-- update induction matrix related outputs
if self.induction[1] ~= nil then
local db = self.induction[1].get_db() ---@type imatrix_session_db
self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW)
self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH)
self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1)
end
end
end
-- update unit tasks
function update.unit_mgmt()
local insufficent_po_rate = false
local need_emcool = false
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
-- update auto waste processing
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
end
end
-- check if unit activated emergency coolant & uses facility tanks
if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then
need_emcool = true
end
end
-- update waste product
self.current_waste_product = self.waste_product
if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then
local db = self.induction[1].get_db() ---@type imatrix_session_db
if db.tanks.energy_fill >= 0.15 then
self.disabled_sps = false
elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then
self.disabled_sps = true
self.current_waste_product = WASTE.POLONIUM
end
else
self.disabled_sps = false
end
if self.pu_fallback and insufficent_po_rate then
self.current_waste_product = WASTE.PLUTONIUM
end
-- make sure dynamic tanks are allowing outflow if required
-- set all, rather than trying to determine which is for which (simpler & safer)
-- there should be no need for any to be in fill only mode
if need_emcool then
for i = 1, #self.tanks do
local session = self.tanks[i] ---@type unit_session
local tank = session.get_db() ---@type dynamicv_session_db
if tank.state.container_mode == CONTAINER_MODE.FILL then
session.get_cmd_queue().push_data(DTV_RTU_S_DATA.SET_CONT_MODE, CONTAINER_MODE.BOTH)
end
end
end
end
--#endregion
-- link the self instance and return the update interface
---@param fac_self _facility_self
return function (fac_self)
self = fac_self
return update
end

View File

@ -0,0 +1,47 @@
--
-- RTU ID Check Failure Entry
--
local types = require("scada-common.types")
local style = require("supervisor.panel.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local ALIGN = core.ALIGN
local cpair = core.cpair
-- create an ID check list entry
---@param parent graphics_element parent
---@param msg string message
---@param fail_code integer failure code
local function init(parent, msg, fail_code)
-- 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=style.theme.highlight_box_bright}
local fg_bg = cpair(colors.black,colors.yellow)
local tag = "MISSING"
if fail_code == types.RTU_ID_FAIL.OUT_OF_RANGE then
fg_bg = cpair(colors.black,colors.orange)
tag = "BAD INDEX"
elseif fail_code == types.RTU_ID_FAIL.DUPLICATE then
fg_bg = cpair(colors.black,colors.red)
tag = "DUPLICATE"
end
TextBox{parent=entry,y=1,text="",width=11,fg_bg=fg_bg}
TextBox{parent=entry,text=tag,alignment=ALIGN.CENTER,width=11,fg_bg=fg_bg}
TextBox{parent=entry,text="",width=11,fg_bg=fg_bg}
TextBox{parent=entry,x=13,y=2,text=msg}
return root
end
return init

View File

@ -10,6 +10,7 @@ local supervisor = require("supervisor.supervisor")
local pgi = require("supervisor.panel.pgi") local pgi = require("supervisor.panel.pgi")
local style = require("supervisor.panel.style") local style = require("supervisor.panel.style")
local chk_entry = require("supervisor.panel.components.chk_entry")
local pdg_entry = require("supervisor.panel.components.pdg_entry") local pdg_entry = require("supervisor.panel.components.pdg_entry")
local rtu_entry = require("supervisor.panel.components.rtu_entry") local rtu_entry = require("supervisor.panel.components.rtu_entry")
@ -73,8 +74,8 @@ local function init(panel)
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg} local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT} local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT} local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@ -83,7 +84,7 @@ local function init(panel)
-- page handling -- page handling
-- --
-- plc page -- plc sessions page
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true} local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49} local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
@ -115,13 +116,13 @@ local function init(panel)
plc_list.line_break() plc_list.line_break()
end end
-- rtu page -- rtu sessions page
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true} local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
local rtu_list = ListBox{parent=rtu_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 rtu_list = ListBox{parent=rtu_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=rtu_list,height=1,hidden=true} -- padding local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding
-- coordinator page -- coordinator session page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true} local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright} local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright}
@ -143,15 +144,37 @@ local function init(panel)
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update) crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor) crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
-- pocket diagnostics page -- pocket sessions page
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true} local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding
-- RTU device ID check/diagnostics page
local chk_page = Div{parent=page_div,x=1,y=1,hidden=true}
local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=chk_list,height=1,hidden=true} -- padding
-- info page
local info_page = Div{parent=page_div,x=1,y=1,hidden=true}
local info = Div{parent=info_page,height=6,x=2,y=2}
TextBox{parent=info,text="SVR \x1a Supervisor Status"}
TextBox{parent=info,text="PLC \x1a Reactor PLC Connections"}
TextBox{parent=info,text="RTU \x1a RTU Gateway Connections"}
TextBox{parent=info,text="CRD \x1a Coordinator Connection"}
TextBox{parent=info,text="PKT \x1a Pocket Connections"}
TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"}
local notes = Div{parent=info_page,width=49,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."}
-- assemble page panes -- assemble page panes
local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page } local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page, chk_page, info_page }
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
@ -161,12 +184,14 @@ local function init(panel)
{ name = "RTU", color = style.fp.text }, { name = "RTU", color = style.fp.text },
{ name = "CRD", color = style.fp.text }, { name = "CRD", color = style.fp.text },
{ name = "PKT", color = style.fp.text }, { name = "PKT", color = style.fp.text },
{ name = "DEV", color = style.fp.text },
{ name = "INF", color = style.fp.text }
} }
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright} TabBar{parent=panel,y=2,tabs=tabs,min_width=7,callback=page_pane.set_value,fg_bg=style.theme.highlight_box_bright}
-- link RTU/PDG list management to PGI -- link RTU/PDG/CHK list management to PGI
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry) pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
end end
return init return init

View File

@ -10,10 +10,12 @@ local pgi = {}
local data = { local data = {
rtu_list = nil, ---@type nil|graphics_element rtu_list = nil, ---@type nil|graphics_element
pdg_list = nil, ---@type nil|graphics_element pdg_list = nil, ---@type nil|graphics_element
chk_list = nil, ---@type nil|graphics_element
rtu_entry = nil, ---@type function rtu_entry = nil, ---@type function
pdg_entry = nil, ---@type function pdg_entry = nil, ---@type function
-- session entries chk_entry = nil, ---@type function
s_entries = { rtu = {}, pdg = {} } -- list entries
entries = { rtu = {}, pdg = {}, chk = {}, missing = {} }
} }
-- link list boxes -- link list boxes
@ -21,19 +23,25 @@ local data = {
---@param rtu_entry function RTU entry constructor ---@param rtu_entry function RTU entry constructor
---@param pdg_list graphics_element pocket diagnostics list element ---@param pdg_list graphics_element pocket diagnostics list element
---@param pdg_entry function pocket diagnostics entry constructor ---@param pdg_entry function pocket diagnostics entry constructor
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry) ---@param chk_list graphics_element CHK list element
---@param chk_entry function CHK entry constructor
function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
data.rtu_list = rtu_list data.rtu_list = rtu_list
data.pdg_list = pdg_list data.pdg_list = pdg_list
data.chk_list = chk_list
data.rtu_entry = rtu_entry data.rtu_entry = rtu_entry
data.pdg_entry = pdg_entry data.pdg_entry = pdg_entry
data.chk_entry = chk_entry
end end
-- unlink all fields, disabling the PGI -- unlink all fields, disabling the PGI
function pgi.unlink() function pgi.unlink()
data.rtu_list = nil data.rtu_list = nil
data.pdg_list = nil data.pdg_list = nil
data.chk_list = nil
data.rtu_entry = nil data.rtu_entry = nil
data.pdg_entry = nil data.pdg_entry = nil
data.chk_entry = nil
end end
-- add an RTU entry to the RTU list -- add an RTU entry to the RTU list
@ -43,7 +51,8 @@ function pgi.create_rtu_entry(session_id)
local success, result = pcall(data.rtu_entry, data.rtu_list, session_id) local success, result = pcall(data.rtu_entry, data.rtu_list, session_id)
if success then if success then
data.s_entries.rtu[session_id] = result data.entries.rtu[session_id] = result
log.debug(util.c("PGI: created RTU entry (", session_id, ")"))
else else
log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true) log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
end end
@ -53,15 +62,17 @@ end
-- delete an RTU entry from the RTU list -- delete an RTU entry from the RTU list
---@param session_id integer RTU session ---@param session_id integer RTU session
function pgi.delete_rtu_entry(session_id) function pgi.delete_rtu_entry(session_id)
if data.s_entries.rtu[session_id] ~= nil then if data.entries.rtu[session_id] ~= nil then
local success, result = pcall(data.s_entries.rtu[session_id].delete) local success, result = pcall(data.entries.rtu[session_id].delete)
data.s_entries.rtu[session_id] = nil data.entries.rtu[session_id] = nil
if not success then if success then
log.debug(util.c("PGI: deleted RTU entry (", session_id, ")"))
else
log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true) log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
end end
else else
log.debug(util.c("PGI: tried to delete unknown RTU entry ", session_id)) log.warning(util.c("PGI: tried to delete unknown RTU entry ", session_id))
end end
end end
@ -72,7 +83,8 @@ function pgi.create_pdg_entry(session_id)
local success, result = pcall(data.pdg_entry, data.pdg_list, session_id) local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
if success then if success then
data.s_entries.pdg[session_id] = result data.entries.pdg[session_id] = result
log.debug(util.c("PGI: created PDG entry (", session_id, ")"))
else else
log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true) log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
end end
@ -82,15 +94,92 @@ end
-- delete a PDG entry from the PDG list -- delete a PDG entry from the PDG list
---@param session_id integer pocket diagnostics session ---@param session_id integer pocket diagnostics session
function pgi.delete_pdg_entry(session_id) function pgi.delete_pdg_entry(session_id)
if data.s_entries.pdg[session_id] ~= nil then if data.entries.pdg[session_id] ~= nil then
local success, result = pcall(data.s_entries.pdg[session_id].delete) local success, result = pcall(data.entries.pdg[session_id].delete)
data.s_entries.pdg[session_id] = nil data.entries.pdg[session_id] = nil
if not success then if success then
log.debug(util.c("PGI: deleted PDG entry (", session_id, ")"))
else
log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true) log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
end end
else else
log.debug(util.c("PGI: tried to delete unknown PDG entry ", session_id)) log.warning(util.c("PGI: tried to delete unknown PDG entry ", session_id))
end
end
-- add a device ID check failure entry to the CHK list
---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case
---@param unit unit_session RTU session
---@param fail_code integer failure code
---@param msg string description to show the user
function pgi.create_chk_entry(unit, fail_code, msg)
local gw_session = unit.get_session_id()
if data.chk_list ~= nil and data.chk_entry ~= nil then
if not data.entries.chk[gw_session] then data.entries.chk[gw_session] = {} end
local success, result = pcall(data.chk_entry, data.chk_list, msg, fail_code)
if success then
data.entries.chk[gw_session][unit.get_unit_id()] = result
log.debug(util.c("PGI: created CHK entry (", gw_session, ":", unit.get_unit_id(), ")"))
else
log.error(util.c("PGI: failed to create CHK entry (", result, ")"), true)
end
end
end
-- delete a device ID check failure entry from the CHK list
---@note this assumes only one type of failure can occur per each RTU gateway session's RTU, which is the case
---@param unit unit_session RTU session
function pgi.delete_chk_entry(unit)
local gw_session = unit.get_session_id()
local ent_chk = data.entries.chk
if ent_chk[gw_session] ~= nil and ent_chk[gw_session][unit.get_unit_id()] ~= nil then
local success, result = pcall(ent_chk[gw_session][unit.get_unit_id()].delete)
ent_chk[gw_session][unit.get_unit_id()] = nil
if success then
log.debug(util.c("PGI: deleted CHK entry ", gw_session, ":", unit.get_unit_id()))
else
log.error(util.c("PGI: failed to delete CHK entry (", result, ")"), true)
end
else
log.warning(util.c("PGI: tried to delete unknown CHK entry with session of ", gw_session, " and unit ID of ", unit.get_unit_id()))
end
end
-- add a device ID missing entry to the CHK list
---@param message string missing device message
function pgi.create_missing_entry(message)
if data.chk_list ~= nil and data.chk_entry ~= nil then
local success, result = pcall(data.chk_entry, data.chk_list, message, 4)
if success then
data.entries.missing[message] = result
log.debug(util.c("PGI: created missing CHK entry (", message, ")"))
else
log.error(util.c("PGI: failed to create missing CHK entry (", result, ")"), true)
end
end
end
-- delete a device ID missing entry from the CHK list
---@param message string missing device message
function pgi.delete_missing_entry(message)
if data.entries.missing[message] ~= nil then
local success, result = pcall(data.entries.missing[message].delete)
data.entries.missing[message] = nil
if success then
log.debug(util.c("PGI: deleted missing CHK entry \"", message, "\""))
else
log.error(util.c("PGI: failed to delete missing CHK entry (", result, ")"), true)
end
else
log.warning(util.c("PGI: tried to delete unknown missing CHK entry \"", message, "\""))
end end
end end

View File

@ -53,7 +53,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "crdn_session(" .. id .. "): " local log_tag = "crdn_session(" .. id .. "): "
local self = { local self = {
units = facility.get_units(), units = facility.get_units(),
@ -184,7 +184,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return return
else else
self.r_seq_num = pkt.scada_frame.seq_num() + 1 self.r_seq_num = pkt.scada_frame.seq_num() + 1
@ -205,7 +205,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
self.last_rtt = srv_now - srv_start self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then if self.last_rtt > 750 then
log.warning(log_header .. "COORD KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_tag .. "COORD KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
@ -213,13 +213,17 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
databus.tx_crd_rtt(self.last_rtt) databus.tx_crd_rtt(self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
---@cast pkt crdn_frame ---@cast pkt crdn_frame
@ -252,7 +256,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
_send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) _send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
else else
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch") log.debug(log_tag .. "CRDN auto start (with configuration) packet length mismatch")
end end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
facility.ack_all() facility.ack_all()
@ -261,25 +265,25 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if pkt.length == 2 then if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) }) _send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) })
else else
log.debug(log_header .. "CRDN set waste mode packet length mismatch") log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
end end
elseif cmd == FAC_COMMAND.SET_PU_FB then elseif cmd == FAC_COMMAND.SET_PU_FB then
if pkt.length == 2 then if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) }) _send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) })
else else
log.debug(log_header .. "CRDN set pu fallback packet length mismatch") log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
end end
elseif cmd == FAC_COMMAND.SET_SPS_LP then elseif cmd == FAC_COMMAND.SET_SPS_LP then
if pkt.length == 2 then if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) }) _send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
else else
log.debug(log_header .. "CRDN set sps low power packet length mismatch") log.debug(log_tag .. "CRDN set sps low power packet length mismatch")
end end
else else
log.debug(log_header .. "CRDN facility command unknown") log.debug(log_tag .. "CRDN facility command unknown")
end end
else else
log.debug(log_header .. "CRDN facility command packet length mismatch") log.debug(log_tag .. "CRDN facility command packet length mismatch")
end end
elseif pkt.type == CRDN_TYPE.UNIT_BUILDS then elseif pkt.type == CRDN_TYPE.UNIT_BUILDS then
-- acknowledgement to coordinator receiving builds -- acknowledgement to coordinator receiving builds
@ -307,13 +311,13 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if pkt.length == 3 then if pkt.length == 3 then
out_queue.push_data(SV_Q_DATA.SET_BURN, data) out_queue.push_data(SV_Q_DATA.SET_BURN, data)
else else
log.debug(log_header .. "CRDN unit command burn rate missing option") log.debug(log_tag .. "CRDN unit command burn rate missing option")
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_mode(pkt.data[3]) unit.set_waste_mode(pkt.data[3])
else else
log.debug(log_header .. "CRDN unit command set waste missing/invalid option") log.debug(log_tag .. "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()
@ -322,32 +326,32 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if pkt.length == 3 then if pkt.length == 3 then
unit.ack_alarm(pkt.data[3]) unit.ack_alarm(pkt.data[3])
else else
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id") log.debug(log_tag .. "CRDN unit command ack alarm missing alarm id")
end end
elseif cmd == UNIT_COMMAND.RESET_ALARM then elseif cmd == UNIT_COMMAND.RESET_ALARM then
if pkt.length == 3 then if pkt.length == 3 then
unit.reset_alarm(pkt.data[3]) unit.reset_alarm(pkt.data[3])
else else
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id") log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end end
elseif cmd == UNIT_COMMAND.SET_GROUP then elseif cmd == UNIT_COMMAND.SET_GROUP 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
facility.set_group(unit.get_id(), pkt.data[3]) facility.set_group(unit.get_id(), pkt.data[3])
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] }) _send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
else else
log.debug(log_header .. "CRDN unit command set group missing group id") log.debug(log_tag .. "CRDN unit command set group missing group id")
end end
else else
log.debug(log_header .. "CRDN unit command unknown") log.debug(log_tag .. "CRDN unit command unknown")
end end
else else
log.debug(log_header .. "CRDN unit command invalid") log.debug(log_tag .. "CRDN unit command invalid")
end end
else else
log.debug(log_header .. "CRDN unit command packet length mismatch") log.debug(log_tag .. "CRDN unit command packet length mismatch")
end end
else else
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type) log.debug(log_tag .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
end end
end end
end end
@ -370,7 +374,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
_close() _close()
_send_mgmt(MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
println("connection to coordinator " .. id .. " closed by server") println("connection to coordinator " .. id .. " closed by server")
log.info(log_header .. "session closed by server") log.info(log_tag .. "session closed by server")
end end
-- iterate the session -- iterate the session
@ -437,14 +441,14 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
_send(CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type) }) _send(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_tag .. "unsupported data command received in in_queue (this is a bug)", true)
end end
end end
end end
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit") log.warning(log_tag .. "exceeded 100ms queue process limit")
break break
end end
end end
@ -452,7 +456,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
println("connection to coordinator closed by remote host") println("connection to coordinator closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_tag .. "session closed by remote host")
return self.connected return self.connected
end end

View File

@ -58,7 +58,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "plc_session(" .. id .. "): " local log_tag = "plc_session(" .. id .. "): "
local self = { local self = {
commanded_state = false, commanded_state = false,
@ -184,7 +184,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)", log.info(util.sprintf(log_tag .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na)) self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na))
end end
@ -289,12 +289,12 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
_copy_status(pkt.data[7]) _copy_status(pkt.data[7])
self.received_status_cache = true self.received_status_cache = true
else else
log.error(log_header .. "RPLC status packet reactor data length mismatch") log.error(log_tag .. "RPLC status packet reactor data length mismatch")
end end
end end
end end
else else
log.debug(log_header .. "RPLC status packet invalid") log.debug(log_tag .. "RPLC status packet invalid")
end end
end end
@ -341,7 +341,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
if pkt.length == 1 then if pkt.length == 1 then
return pkt.data[1] return pkt.data[1]
else else
log.debug(log_header .. "RPLC ACK length mismatch") log.debug(log_tag .. "RPLC ACK length mismatch")
return nil return nil
end end
end end
@ -351,7 +351,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return return
else else
self.r_seq_num = pkt.scada_frame.seq_num() + 1 self.r_seq_num = pkt.scada_frame.seq_num() + 1
@ -362,7 +362,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
---@cast pkt rplc_frame ---@cast pkt rplc_frame
-- check reactor ID -- check reactor ID
if pkt.id ~= reactor_id then if pkt.id ~= reactor_id then
log.warning(log_header .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id) log.warning(log_tag .. "discarding RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
return return
end end
@ -375,7 +375,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
if pkt.length >= 5 then if pkt.length >= 5 then
_handle_status(pkt) _handle_status(pkt)
else else
log.debug(log_header .. "RPLC status packet length mismatch") log.debug(log_tag .. "RPLC status packet length mismatch")
end end
elseif pkt.type == RPLC_TYPE.MEK_STRUCT then elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
-- received reactor structure, record it -- received reactor structure, record it
@ -385,7 +385,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.received_struct = true self.received_struct = true
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id) out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else else
log.debug(log_header .. "RPLC struct packet length mismatch") log.debug(log_tag .. "RPLC struct packet length mismatch")
end end
elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
-- burn rate acknowledgement -- burn rate acknowledgement
@ -393,7 +393,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
if ack then if ack then
self.acks.burn_rate = true self.acks.burn_rate = true
elseif ack == false then elseif ack == false then
log.debug(log_header .. "burn rate update failed!") log.debug(log_tag .. "burn rate update failed!")
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
@ -408,7 +408,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
if ack then if ack then
self.sDB.control_state = true self.sDB.control_state = true
elseif ack == false then elseif ack == false then
log.debug(log_header .. "enable failed!") log.debug(log_tag .. "enable failed!")
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
@ -424,7 +424,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.acks.disable = true self.acks.disable = true
self.sDB.control_state = false self.sDB.control_state = false
elseif ack == false then elseif ack == false then
log.debug(log_header .. "disable failed!") log.debug(log_tag .. "disable failed!")
end end
elseif pkt.type == RPLC_TYPE.RPS_SCRAM then elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
-- manual SCRAM acknowledgement -- manual SCRAM acknowledgement
@ -433,7 +433,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.acks.scram = true self.acks.scram = true
self.sDB.control_state = false self.sDB.control_state = false
elseif ack == false then elseif ack == false then
log.debug(log_header .. "manual SCRAM failed!") log.debug(log_tag .. "manual SCRAM failed!")
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
@ -449,7 +449,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.acks.ascram = true self.acks.ascram = true
self.sDB.control_state = false self.sDB.control_state = false
elseif ack == false then elseif ack == false then
log.debug(log_header .. " automatic SCRAM failed!") log.debug(log_tag .. " automatic SCRAM failed!")
end end
elseif pkt.type == RPLC_TYPE.RPS_STATUS then elseif pkt.type == RPLC_TYPE.RPS_STATUS then
-- RPS status packet received, copy data -- RPS status packet received, copy data
@ -459,10 +459,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
-- copied in RPS status data OK -- copied in RPS status data OK
else else
-- error copying RPS status data -- error copying RPS status data
log.error(log_header .. "failed to parse RPS status packet data") log.error(log_tag .. "failed to parse RPS status packet data")
end end
else else
log.debug(log_header .. "RPLC RPS status packet length mismatch") log.debug(log_tag .. "RPLC RPS status packet length mismatch")
end end
elseif pkt.type == RPLC_TYPE.RPS_ALARM then elseif pkt.type == RPLC_TYPE.RPS_ALARM then
-- RPS alarm -- RPS alarm
@ -472,10 +472,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
-- copied in RPS status data OK -- copied in RPS status data OK
else else
-- error copying RPS status data -- error copying RPS status data
log.error(log_header .. "failed to parse RPS alarm status data") log.error(log_tag .. "failed to parse RPS alarm status data")
end end
else else
log.debug(log_header .. "RPLC RPS alarm packet length mismatch") log.debug(log_tag .. "RPLC RPS alarm packet length mismatch")
end end
elseif pkt.type == RPLC_TYPE.RPS_RESET then elseif pkt.type == RPLC_TYPE.RPS_RESET then
-- RPS reset acknowledgement -- RPS reset acknowledgement
@ -485,7 +485,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.sDB.rps_tripped = false self.sDB.rps_tripped = false
self.sDB.rps_trip_cause = "ok" self.sDB.rps_trip_cause = "ok"
elseif ack == false then elseif ack == false then
log.debug(log_header .. "RPS reset failed") log.debug(log_tag .. "RPS reset failed")
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
@ -498,7 +498,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
-- RPS auto control reset acknowledgement -- RPS auto control reset acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if not ack then if not ack then
log.debug(log_header .. "RPS auto reset failed") log.debug(log_tag .. "RPS auto reset failed")
end end
elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then
if pkt.length == 1 then if pkt.length == 1 then
@ -506,18 +506,18 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
if ack == PLC_AUTO_ACK.FAIL then if ack == PLC_AUTO_ACK.FAIL then
self.acks.burn_rate = false self.acks.burn_rate = false
log.debug(log_header .. "RPLC automatic burn rate set fail") log.debug(log_tag .. "RPLC automatic burn rate set fail")
elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK or ack == PLC_AUTO_ACK.RAMP_SET_OK or ack == PLC_AUTO_ACK.ZERO_DIS_OK then elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK or ack == PLC_AUTO_ACK.RAMP_SET_OK or ack == PLC_AUTO_ACK.ZERO_DIS_OK then
self.acks.burn_rate = true self.acks.burn_rate = true
else else
self.acks.burn_rate = false self.acks.burn_rate = false
log.debug(log_header .. "RPLC automatic burn rate ack unknown") log.debug(log_tag .. "RPLC automatic burn rate ack unknown")
end end
else else
log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch") log.debug(log_tag .. "RPLC automatic burn rate ack packet length mismatch")
end end
else else
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported RPLC packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame ---@cast pkt mgmt_frame
@ -530,7 +530,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
self.last_rtt = srv_now - srv_start self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then if self.last_rtt > 750 then
log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_tag .. "PLC KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
@ -538,13 +538,17 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
databus.tx_plc_rtt(reactor_id, self.last_rtt) databus.tx_plc_rtt(reactor_id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end
end end
end end
@ -639,7 +643,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
_close() _close()
_send_mgmt(MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
println("connection to reactor " .. reactor_id .. " PLC closed by server") println("connection to reactor " .. reactor_id .. " PLC closed by server")
log.info(log_header .. "session closed by server") log.info(log_tag .. "session closed by server")
end end
-- iterate the session -- iterate the session
@ -696,7 +700,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
_send(RPLC_TYPE.RPS_AUTO_RESET, {}) _send(RPLC_TYPE.RPS_AUTO_RESET, {})
end end
else else
log.error(log_header .. "unsupported command received in in_queue (this is a bug)", true) log.error(log_tag .. "unsupported command received in in_queue (this is a bug)", true)
end end
elseif message.qtype == mqueue.TYPE.DATA then elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body -- instruction with body
@ -745,14 +749,14 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
end end
end end
else else
log.error(log_header .. "unsupported data command received in in_queue (this is a bug)", true) log.error(log_tag .. "unsupported data command received in in_queue (this is a bug)", true)
end end
end end
end end
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit") log.warning(log_tag .. "exceeded 100ms queue process limit")
break break
end end
end end
@ -760,7 +764,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
println("connection to reactor " .. reactor_id .. " PLC closed by remote host") println("connection to reactor " .. reactor_id .. " PLC closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_tag .. "session closed by remote host")
return self.connected return self.connected
end end
@ -802,7 +806,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
if not self.received_status_cache then if not self.received_status_cache then
if rtimes.status_req - util.time() <= 0 then if rtimes.status_req - util.time() <= 0 then
_send(RPLC_TYPE.MEK_STATUS, {}) _send(RPLC_TYPE.STATUS, {})
rtimes.status_req = util.time() + RETRY_PERIOD rtimes.status_req = util.time() + RETRY_PERIOD
end end
end end

View File

@ -40,7 +40,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "pdg_session(" .. id .. "): " local log_tag = "pdg_session(" .. id .. "): "
local self = { local self = {
-- connection properties -- connection properties
@ -95,7 +95,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return return
else else
self.r_seq_num = pkt.scada_frame.seq_num() + 1 self.r_seq_num = pkt.scada_frame.seq_num() + 1
@ -116,7 +116,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
self.last_rtt = srv_now - srv_start self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then if self.last_rtt > 750 then
log.warning(log_header .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_tag .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms") -- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms")
@ -124,11 +124,15 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
databus.tx_pdg_rtt(id, self.last_rtt) databus.tx_pdg_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
elseif pkt.type == MGMT_TYPE.DIAG_TONE_GET then elseif pkt.type == MGMT_TYPE.DIAG_TONE_GET then
-- get the state of alarm tones -- get the state of alarm tones
_send_mgmt(MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones()) _send_mgmt(MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones())
@ -145,13 +149,13 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2]) local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2])
_send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states }) _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
else else
log.debug(log_header .. "SCADA diag tone set packet data type mismatch") log.debug(log_tag .. "SCADA diag tone set packet data type mismatch")
end end
else else
log.debug(log_header .. "SCADA diag tone set packet length mismatch") log.debug(log_tag .. "SCADA diag tone set packet length mismatch")
end end
else else
log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security") log.debug(log_tag .. "DIAG_TONE_SET is blocked without HMAC for security")
end end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { false }) end if not valid then _send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { false }) end
@ -168,18 +172,18 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2]) local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2])
_send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states }) _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
else else
log.debug(log_header .. "SCADA diag alarm set packet data type mismatch") log.debug(log_tag .. "SCADA diag alarm set packet data type mismatch")
end end
else else
log.debug(log_header .. "SCADA diag alarm set packet length mismatch") log.debug(log_tag .. "SCADA diag alarm set packet length mismatch")
end end
else else
log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security") log.debug(log_tag .. "DIAG_ALARM_SET is blocked without HMAC for security")
end end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end
end end
end end
@ -205,7 +209,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
_close() _close()
_send_mgmt(MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
println("connection to pocket diag session " .. id .. " closed by server") println("connection to pocket diag session " .. id .. " closed by server")
log.info(log_header .. "session closed by server") log.info(log_tag .. "session closed by server")
end end
-- iterate the session -- iterate the session
@ -236,7 +240,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit") log.warning(log_tag .. "exceeded 100ms queue process limit")
break break
end end
end end
@ -244,7 +248,7 @@ function pocket.new_session(id, s_addr, i_seq_num, 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 diag session " .. id .. " closed by remote host") println("connection to pocket diag session " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_tag .. "session closed by remote host")
return self.connected return self.connected
end end

View File

@ -30,7 +30,7 @@ local PERIODICS = {
ALARM_TONES = 500 ALARM_TONES = 500
} }
-- create a new RTU session -- create a new RTU gateway session
---@nodiscard ---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param s_addr integer device source address ---@param s_addr integer device source address
@ -38,14 +38,14 @@ local PERIODICS = {
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
---@param advertisement table RTU device advertisement ---@param advertisement table RTU gateway device advertisement
---@param facility facility facility data table ---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running ---@param fp_ok boolean if the front panel UI is running
function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, fp_ok) function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end local function println(message) if not fp_ok then util.println_ts(message) end end
local log_header = "rtu_session(" .. id .. "): " local log_tag = "rtu_gw_session(" .. id .. "): "
local self = { local self = {
modbus_q = mqueue.new(), modbus_q = mqueue.new(),
@ -124,7 +124,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
if u_type == false then if u_type == false then
-- validation fail -- validation fail
log.debug(log_header .. "_handle_advertisement(): advertisement unit validation failure") log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
else else
if unit_advert.reactor > 0 then if unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
@ -156,9 +156,9 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
if type(unit) ~= "nil" then target_unit.add_envd(unit) end if type(unit) ~= "nil" then target_unit.add_envd(unit) end
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
-- skip virtual units -- skip virtual units
log.debug(util.c(log_header, "skipping virtual RTU unit #", i)) log.debug(util.c(log_tag, "skipping virtual RTU #", i))
else else
log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string)) log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string))
end end
else else
-- facility RTUs -- facility RTUs
@ -184,9 +184,9 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
if type(unit) ~= "nil" then facility.add_envd(unit) end if type(unit) ~= "nil" then facility.add_envd(unit) end
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
-- skip virtual units -- skip virtual units
log.debug(util.c(log_header, "skipping virtual RTU unit #", i)) log.debug(util.c(log_tag, "skipping virtual RTU #", i))
else else
log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string)) log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string))
end end
end end
end end
@ -195,20 +195,20 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
self.units[i] = unit self.units[i] = unit
unit_count = unit_count + 1 unit_count = unit_count + 1
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
log.warning(util.c(log_header, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")")) log.warning(util.c(log_tag, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")"))
end end
end end
databus.tx_rtu_units(id, unit_count) databus.tx_rtu_units(id, unit_count)
end end
-- mark this RTU session as closed, stop watchdog -- mark this RTU gateway session as closed, stop watchdog
local function _close() local function _close()
self.conn_watchdog.cancel() self.conn_watchdog.cancel()
self.connected = false self.connected = false
databus.tx_rtu_disconnected(id) databus.tx_rtu_disconnected(id)
-- mark all RTU unit sessions as closed so the reactor unit knows -- mark all RTU sessions as closed so the reactor unit knows
for _, unit in pairs(self.units) do unit.close() end for _, unit in pairs(self.units) do unit.close() end
end end
@ -242,7 +242,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return return
else else
self.r_seq_num = pkt.scada_frame.seq_num() + 1 self.r_seq_num = pkt.scada_frame.seq_num() + 1
@ -265,27 +265,31 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
-- local rtu_send = pkt.data[2] -- local rtu_gw_send = pkt.data[2]
local srv_now = util.time() local srv_now = util.time()
self.last_rtt = srv_now - srv_start self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then if self.last_rtt > 750 then
log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") log.warning(log_tag .. "RTU GW KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end end
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms") -- log.debug(log_tag .. "RTU GW RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms") -- log.debug(log_tag .. "RTU GW TT = " .. (srv_now - rtu_gw_send) .. "ms")
databus.tx_rtu_rtt(id, self.last_rtt) databus.tx_rtu_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
elseif pkt.type == MGMT_TYPE.RTU_ADVERT then elseif pkt.type == MGMT_TYPE.RTU_ADVERT then
-- RTU unit advertisement -- RTU advertisement
log.debug(log_header .. "received updated advertisement") log.debug(log_tag .. "received updated advertisement")
self.advert = pkt.data self.advert = pkt.data
-- handle advertisement; this will re-create all unit sub-sessions -- handle advertisement; this will re-create all unit sub-sessions
@ -298,17 +302,17 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
unit.invalidate_cache() unit.invalidate_cache()
end end
else else
log.debug(log_header .. "SCADA RTU device re-mount packet length mismatch") log.debug(log_tag .. "SCADA RTU GW device re-mount packet length mismatch")
end end
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end
end end
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- get the session ID -- get the gateway session ID
function public.get_id() return id end function public.get_id() return id end
-- check if a timer matches this session's watchdog -- check if a timer matches this session's watchdog
@ -322,8 +326,8 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
function public.close() function public.close()
_close() _close()
_send_mgmt(MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
println(log_header .. "connection to RTU closed by server") println(log_tag .. "connection to RTU GW closed by server")
log.info(log_header .. "session closed by server") log.info(log_tag .. "session closed by server")
end end
-- iterate the session -- iterate the session
@ -354,7 +358,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
-- max 100ms spent processing queue -- max 100ms spent processing queue
if util.time() - handle_start > 100 then if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit") log.warning(log_tag .. "exceeded 100ms queue process limit")
break break
end end
end end
@ -362,7 +366,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
-- exit if connection was closed -- exit if connection was closed
if not self.connected then if not self.connected then
println("RTU connection " .. id .. " closed by remote host") println("RTU connection " .. id .. " closed by remote host")
log.info(log_header .. "session closed by remote host") log.info(log_tag .. "session closed by remote host")
return self.connected return self.connected
end end

View File

@ -32,10 +32,10 @@ local PERIODICS = {
-- create a new boilerv rtu session runner -- create a new boilerv rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU message out queue
function boilerv.new(session_id, unit_id, advert, out_queue) function boilerv.new(session_id, unit_id, advert, out_queue)
-- checks -- checks
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then

View File

@ -44,10 +44,10 @@ local PERIODICS = {
-- create a new dynamicv rtu session runner -- create a new dynamicv rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU message out queue
function dynamicv.new(session_id, unit_id, advert, out_queue) function dynamicv.new(session_id, unit_id, advert, out_queue)
-- checks -- checks
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then

View File

@ -23,10 +23,10 @@ local PERIODICS = {
-- create a new environment detector rtu session runner -- create a new environment detector rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer ---@param session_id integer RTU gateway session ID
---@param unit_id integer ---@param unit_id integer RTU ID
---@param advert rtu_advertisement ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue ---@param out_queue mqueue RTU message out queue
function envd.new(session_id, unit_id, advert, out_queue) function envd.new(session_id, unit_id, advert, out_queue)
-- checks -- checks
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then

View File

@ -32,10 +32,10 @@ local PERIODICS = {
-- create a new imatrix rtu session runner -- create a new imatrix rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU message out queue
function imatrix.new(session_id, unit_id, advert, out_queue) function imatrix.new(session_id, unit_id, advert, out_queue)
-- checks -- checks
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then if advert.type ~= RTU_UNIT_TYPE.IMATRIX then

View File

@ -45,10 +45,10 @@ local PERIODICS = {
-- create a new redstone rtu session runner -- create a new redstone rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer ---@param session_id integer RTU gateway session ID
---@param unit_id integer ---@param unit_id integer RTU ID
---@param advert rtu_advertisement ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue ---@param out_queue mqueue RTU message out queue
function redstone.new(session_id, unit_id, advert, out_queue) function redstone.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then if advert.type ~= RTU_UNIT_TYPE.REDSTONE then

View File

@ -29,10 +29,10 @@ local PERIODICS = {
-- create a new sna rtu session runner -- create a new sna rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU message out queue
function sna.new(session_id, unit_id, advert, out_queue) function sna.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.SNA then if advert.type ~= RTU_UNIT_TYPE.SNA then

View File

@ -32,10 +32,10 @@ local PERIODICS = {
-- create a new sps rtu session runner -- create a new sps rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU message out queue
function sps.new(session_id, unit_id, advert, out_queue) function sps.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPE.SPS then if advert.type ~= RTU_UNIT_TYPE.SPS then

View File

@ -44,10 +44,10 @@ local PERIODICS = {
-- create a new turbinev rtu session runner -- create a new turbinev rtu session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU message out queue
function turbinev.new(session_id, unit_id, advert, out_queue) function turbinev.new(session_id, unit_id, advert, out_queue)
-- checks -- checks
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then

View File

@ -24,7 +24,7 @@ unit_session.RTU_US_DATA = RTU_US_DATA
-- create a new unit session runner -- create a new unit session runner
---@nodiscard ---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU gateway session ID
---@param unit_id integer MODBUS unit ID ---@param unit_id integer MODBUS unit ID
---@param advert rtu_advertisement RTU advertisement for this unit ---@param advert rtu_advertisement RTU advertisement for this unit
---@param out_queue mqueue send queue ---@param out_queue mqueue send queue
@ -144,12 +144,15 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- get the unit ID -- get the RTU gateway session ID
---@nodiscard ---@nodiscard
function public.get_session_id() return session_id end function public.get_session_id() return session_id end
-- get the unit ID -- get the unit ID
---@nodiscard ---@nodiscard
function public.get_unit_id() return unit_id end function public.get_unit_id() return unit_id end
-- get the RTU type
---@nodiscard
function public.get_unit_type() return advert.type end
-- get the device index -- get the device index
---@nodiscard ---@nodiscard
function public.get_device_idx() return self.device_index or 0 end function public.get_device_idx() return self.device_index or 0 end

View File

@ -1,9 +1,15 @@
--
-- Supervisor Sessions Handler
--
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")
local facility = require("supervisor.facility")
local pgi = require("supervisor.panel.pgi")
local coordinator = require("supervisor.session.coordinator") local coordinator = require("supervisor.session.coordinator")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
@ -11,13 +17,15 @@ local pocket = require("supervisor.session.pocket")
local rtu = require("supervisor.session.rtu") local rtu = require("supervisor.session.rtu")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
-- Supervisor Sessions Handler local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_TYPES = types.RTU_UNIT_TYPE
local SV_Q_DATA = svqtypes.SV_Q_DATA local SV_Q_DATA = svqtypes.SV_Q_DATA
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
local PLC_S_DATA = plc.PLC_S_DATA local PLC_S_DATA = plc.PLC_S_DATA
local CRD_S_DATA = coordinator.CRD_S_DATA
local CRD_S_DATA = coordinator.CRD_S_DATA
local svsessions = {} local svsessions = {}
@ -37,12 +45,13 @@ local self = {
config = nil, ---@type svr_config config = nil, ---@type svr_config
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} }, sessions = { rtu = {}, plc = {}, crd = {}, pdg = {} },
next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 } next_ids = { rtu = 0, plc = 0, crd = 0, pdg = 0 },
dev_dbg = { duplicate = {}, out_of_range = {}, connected = {} }
} }
---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct ---@alias sv_session_structs plc_session_struct|rtu_session_struct|crd_session_struct|pdg_session_struct
-- PRIVATE FUNCTIONS -- --#region PRIVATE FUNCTIONS
-- handle a session output queue -- handle a session output queue
---@param session sv_session_structs ---@param session sv_session_structs
@ -190,18 +199,184 @@ local function _find_session(list, s_addr)
return nil return nil
end end
-- PUBLIC FUNCTIONS -- -- periodically remove disconnected RTU gateway's RTU ID warnings and update the missing device list
local function _update_dev_dbg()
-- remove disconnected units from check failures lists
local f = function (unit) return unit.is_connected() end
util.filter_table(self.dev_dbg.duplicate, f, pgi.delete_chk_entry)
util.filter_table(self.dev_dbg.out_of_range, f, pgi.delete_chk_entry)
-- update missing list
local conns = self.dev_dbg.connected
local units = self.facility.get_units()
local rtu_conns = self.facility.check_rtu_conns()
local function report(disconnected, msg)
if disconnected then pgi.create_missing_entry(msg) else pgi.delete_missing_entry(msg) end
end
-- look for disconnected facility RTUs
if rtu_conns.induction ~= conns.induction then
report(conns.induction, util.c("the facility's induction matrix"))
conns.induction = rtu_conns.induction
end
if rtu_conns.sps ~= conns.sps then
report(conns.sps, util.c("the facility's SPS"))
conns.sps = rtu_conns.sps
end
for i = 1, #conns.tanks do
if (rtu_conns.tanks[i] or false) ~= conns.tanks[i] then
report(conns.tanks[i], util.c("the facility's #", i, " dynamic tank"))
conns.tanks[i] = rtu_conns.tanks[i] or false
end
end
-- look for disconnected unit RTUs
for u = 1, #units do
local u_conns = conns.units[u]
rtu_conns = units[u].check_rtu_conns()
for i = 1, #u_conns.boilers do
if (rtu_conns.boilers[i] or false) ~= u_conns.boilers[i] then
report(u_conns.boilers[i], util.c("unit ", u, "'s #", i, " boiler"))
u_conns.boilers[i] = rtu_conns.boilers[i] or false
end
end
for i = 1, #u_conns.turbines do
if (rtu_conns.turbines[i] or false) ~= u_conns.turbines[i] then
report(u_conns.turbines[i], util.c("unit ", u, "'s #", i, " turbine"))
u_conns.turbines[i] = rtu_conns.turbines[i] or false
end
end
for i = 1, #u_conns.tanks do
if (rtu_conns.tanks[i] or false) ~= u_conns.tanks[i] then
report(u_conns.tanks[i], util.c("unit ", u, "'s dynamic tank"))
u_conns.tanks[i] = rtu_conns.tanks[i] or false
end
end
end
end
--#endregion
--#region PUBLIC FUNCTIONS
-- on attempted link of an RTU to a facility or unit object, verify its ID and report a problem if it can't be accepted
---@param unit unit_session RTU session
---@param list table table of RTU sessions
---@param max integer max of this type of RTU
---@return RTU_ID_FAIL fail_code, string fail_str
function svsessions.check_rtu_id(unit, list, max)
local fail_code, fail_str = RTU_ID_FAIL.OK, "OK"
if (unit.get_device_idx() < 1 and max ~= 1) or unit.get_device_idx() > max then
-- out-of-range
fail_code, fail_str = RTU_ID_FAIL.OUT_OF_RANGE, "index out of range"
table.insert(self.dev_dbg.out_of_range, unit)
else
for _, u in ipairs(list) do
if u.get_device_idx() == unit.get_device_idx() then
-- duplicate
fail_code, fail_str = RTU_ID_FAIL.DUPLICATE, "duplicate index"
table.insert(self.dev_dbg.duplicate, unit)
break
end
end
end
-- make sure this won't exceed the maximum allowable devices
if fail_code == RTU_ID_FAIL.OK and #list >= max then
fail_code, fail_str = RTU_ID_FAIL.MAX_DEVICES, "too many of this type"
end
-- add to the list for the user
if fail_code ~= RTU_ID_FAIL.OK and fail_code ~= RTU_ID_FAIL.MAX_DEVICES then
local r_id, idx, type = unit.get_reactor(), unit.get_device_idx(), unit.get_unit_type()
local msg
if r_id == 0 then
msg = "the facility's "
if type == RTU_TYPES.IMATRIX then
msg = msg .. "induction matrix"
elseif type == RTU_TYPES.SPS then
msg = msg .. "SPS"
elseif type == RTU_TYPES.DYNAMIC_VALVE then
msg = util.c(msg, "#", idx, " dynamic tank")
elseif type == RTU_TYPES.ENV_DETECTOR then
msg = util.c(msg, "#", idx, " env. detector")
else
msg = msg .. " ? (error)"
end
else
msg = util.c("unit ", r_id, "'s ")
if type == RTU_TYPES.BOILER_VALVE then
msg = util.c(msg, "#", idx, " boiler")
elseif type == RTU_TYPES.TURBINE_VALVE then
msg = util.c(msg, "#", idx, " turbine")
elseif type == RTU_TYPES.DYNAMIC_VALVE then
msg = msg .. "dynamic tank"
elseif type == RTU_TYPES.ENV_DETECTOR then
msg = util.c(msg, "#", idx, " env. detector")
else
msg = msg .. " ? (error)"
end
end
pgi.create_chk_entry(unit, fail_code, msg)
end
return fail_code, fail_str
end
-- initialize svsessions -- initialize svsessions
---@param nic nic network interface device ---@param nic nic network interface device
---@param fp_ok boolean front panel active ---@param fp_ok boolean front panel active
---@param config svr_config supervisor configuration ---@param config svr_config supervisor configuration
---@param cooling_conf sv_cooling_conf cooling configuration definition ---@param facility facility
function svsessions.init(nic, fp_ok, config, cooling_conf) function svsessions.init(nic, fp_ok, config, facility)
self.nic = nic self.nic = nic
self.fp_ok = fp_ok self.fp_ok = fp_ok
self.config = config self.config = config
self.facility = facility.new(config, cooling_conf) self.facility = facility
-- initialize connection tracking table by setting all expected devices to true
-- if connections are missing, missing entries will then be created on the next update
self.dev_dbg.connected = { induction = true, sps = true, tanks = {}, units = {} }
local cool_conf = facility.get_cooling_conf()
for i = 1, #cool_conf.fac_tank_list do
if cool_conf.fac_tank_list[i] == 2 then
table.insert(self.dev_dbg.connected.tanks, true)
end
end
for i = 1, config.UnitCount do
local r_cool = cool_conf.r_cool[i]
local conns = { boilers = {}, turbines = {}, tanks = {} }
for b = 1, r_cool.BoilerCount do conns.boilers[b] = true end
for t = 1, r_cool.TurbineCount do conns.turbines[t] = true end
if r_cool.TankConnection and cool_conf.fac_tank_defs[i] == 1 then
conns.tanks[1] = true
end
self.dev_dbg.connected.units[i] = conns
end
end end
-- find an RTU session by the computer ID -- find an RTU session by the computer ID
@ -466,6 +641,9 @@ function svsessions.iterate_all()
-- iterate units -- iterate units
self.facility.update_units() self.facility.update_units()
-- update tracking of bad RTU IDs and missing devices
_update_dev_dbg()
end end
-- delete all closed sessions -- delete all closed sessions
@ -482,4 +660,6 @@ function svsessions.close_all()
svsessions.free_all_closed() svsessions.free_all_closed()
end end
--#endregion
return svsessions return svsessions

View File

@ -16,12 +16,13 @@ local core = require("graphics.core")
local configure = require("supervisor.configure") local configure = require("supervisor.configure")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local facility = require("supervisor.facility")
local renderer = require("supervisor.renderer") local renderer = require("supervisor.renderer")
local supervisor = require("supervisor.supervisor") local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.4.2" local SUPERVISOR_VERSION = "v1.5.2"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@ -129,9 +130,12 @@ local function main()
println_ts = function (_) end println_ts = function (_) end
end end
-- create facility and unit objects
local sv_facility = facility.new(config)
-- create network interface then setup comms -- create network interface then setup comms
local nic = network.nic(modem) local nic = network.nic(modem)
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok) local superv_comms = supervisor.comms(SUPERVISOR_VERSION, nic, fp_ok, sv_facility)
-- base loop clock (6.67Hz, 3 ticks) -- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15 local MAIN_CLOCK = 0.15

View File

@ -102,14 +102,12 @@ end
---@param _version string supervisor version ---@param _version string supervisor version
---@param nic nic network interface device ---@param nic nic network interface device
---@param fp_ok boolean if the front panel UI is running ---@param fp_ok boolean if the front panel UI is running
---@param facility facility facility instance
---@diagnostic disable-next-line: unused-local ---@diagnostic disable-next-line: unused-local
function supervisor.comms(_version, nic, fp_ok) function supervisor.comms(_version, nic, fp_ok, facility)
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end local function println(message) if not fp_ok then util.println_ts(message) end end
---@class sv_cooling_conf
local cooling_conf = { r_cool = config.CoolingConfig, fac_tank_mode = config.FacilityTankMode, fac_tank_defs = config.FacilityTankDefs }
local self = { local self = {
last_est_acks = {} last_est_acks = {}
} }
@ -122,8 +120,8 @@ function supervisor.comms(_version, nic, fp_ok)
nic.closeAll() nic.closeAll()
nic.open(config.SVR_Channel) nic.open(config.SVR_Channel)
-- pass modem, status, and config data to svsessions -- pass system data and objects to svsessions
svsessions.init(nic, fp_ok, config, cooling_conf) svsessions.init(nic, fp_ok, config, facility)
-- send an establish request response -- send an establish request response
---@param packet scada_packet ---@param packet scada_packet
@ -373,7 +371,7 @@ function supervisor.comms(_version, nic, fp_ok)
println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected")) println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected"))
log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, cooling_conf }) _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, { config.UnitCount, facility.get_cooling_conf() })
else else
if last_ack ~= ESTABLISH_ACK.COLLISION then if last_ack ~= ESTABLISH_ACK.COLLISION then
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator") log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")

View File

@ -1,12 +1,13 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local logic = require("supervisor.unitlogic") local logic = require("supervisor.unitlogic")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local rsctl = require("supervisor.session.rsctl") local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions")
local WASTE_MODE = types.WASTE_MODE local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT local WASTE = types.WASTE_PRODUCT
@ -14,6 +15,7 @@ local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL local TRI_FAIL = types.TRI_FAIL
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE 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,6 +70,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- time (ms) to idle for auto idling -- time (ms) to idle for auto idling
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000) local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
local log_tag = "UNIT " .. reactor_id .. ": "
---@class _unit_self ---@class _unit_self
local self = { local self = {
r_id = reactor_id, r_id = reactor_id,
@ -264,7 +268,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
table.insert(self.db.annunciator.TurbineOverSpeed, false) table.insert(self.db.annunciator.TurbineOverSpeed, false)
table.insert(self.db.annunciator.GeneratorTrip, false) table.insert(self.db.annunciator.GeneratorTrip, false)
table.insert(self.db.annunciator.TurbineTrip, false) table.insert(self.db.annunciator.TurbineTrip, false)
table.insert(self.turbine_stability_data, { time_state = 0, time_tanks = 0, rotation = 1 }) table.insert(self.turbine_stability_data, { time_state = 0, time_tanks = 0, rotation = 1, input_rate = 0 })
end end
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
@ -420,6 +424,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
self.plc_s = plc_session self.plc_s = plc_session
self.plc_i = plc_session.instance self.plc_i = plc_session.instance
log.debug(util.c(log_tag, "linked PLC [", plc_session.s_addr, ":", plc_session.r_chan, "]"))
-- reset deltas -- reset deltas
_reset_dt(DT_KEYS.ReactorTemp) _reset_dt(DT_KEYS.ReactorTemp)
_reset_dt(DT_KEYS.ReactorFuel) _reset_dt(DT_KEYS.ReactorFuel)
@ -432,6 +438,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
---@param rs_unit unit_session ---@param rs_unit unit_session
function public.add_redstone(rs_unit) function public.add_redstone(rs_unit)
table.insert(self.redstone, rs_unit) table.insert(self.redstone, rs_unit)
log.debug(util.c(log_tag, "linked redstone [", rs_unit.get_unit_id(), "@", rs_unit.get_session_id(), "]"))
-- send or re-send waste settings -- send or re-send waste settings
_set_waste_valves(self.waste_product) _set_waste_valves(self.waste_product)
@ -441,42 +448,61 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
---@param turbine unit_session ---@param turbine unit_session
---@return boolean linked turbine accepted to associated device slot ---@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 local fail_code, fail_str = svsessions.check_rtu_id(turbine, self.turbines, num_turbines)
local ok = fail_code == RTU_ID_FAIL.OK
if ok then
table.insert(self.turbines, turbine) table.insert(self.turbines, turbine)
log.debug(util.c(log_tag, "linked turbine #", turbine.get_device_idx(), " [", turbine.get_unit_id(), "@", turbine.get_session_id(), "]"))
-- reset deltas -- reset deltas
_reset_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx()) _reset_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx())
_reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx()) _reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx())
else
log.warning(util.c(log_tag, "rejected turbine linking due to failure code ", fail_code, " (", fail_str, ")"))
end
return true return ok
else 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 ---@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 local fail_code, fail_str = svsessions.check_rtu_id(boiler, self.boilers, num_boilers)
local ok = fail_code == RTU_ID_FAIL.OK
if ok then
table.insert(self.boilers, boiler) table.insert(self.boilers, boiler)
log.debug(util.c(log_tag, "linked boiler #", boiler.get_device_idx(), " [", boiler.get_unit_id(), "@", boiler.get_session_id(), "]"))
-- reset deltas -- reset deltas
_reset_dt(DT_KEYS.BoilerWater .. boiler.get_device_idx()) _reset_dt(DT_KEYS.BoilerWater .. boiler.get_device_idx())
_reset_dt(DT_KEYS.BoilerSteam .. boiler.get_device_idx()) _reset_dt(DT_KEYS.BoilerSteam .. boiler.get_device_idx())
_reset_dt(DT_KEYS.BoilerCCool .. boiler.get_device_idx()) _reset_dt(DT_KEYS.BoilerCCool .. boiler.get_device_idx())
_reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx()) _reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx())
else
log.warning(util.c(log_tag, "rejected boiler linking due to failure code ", fail_code, " (", fail_str, ")"))
end
return true return ok
else return false end
end end
-- link a dynamic tank RTU session -- link a dynamic tank RTU session
---@param dynamic_tank unit_session ---@param dynamic_tank unit_session
---@return boolean linked dynamic tank accepted (max 1) ---@return boolean linked dynamic tank accepted (max 1)
function public.add_tank(dynamic_tank) function public.add_tank(dynamic_tank)
if #self.tanks == 0 then local fail_code, fail_str = svsessions.check_rtu_id(dynamic_tank, self.tanks, 1)
local ok = fail_code == RTU_ID_FAIL.OK
if ok then
table.insert(self.tanks, dynamic_tank) table.insert(self.tanks, dynamic_tank)
return true log.debug(util.c(log_tag, "linked dynamic tank [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]"))
else return false end else
log.warning(util.c(log_tag, "rejected dynamic tank linking due to failure code ", fail_code, " (", fail_str, ")"))
end
return ok
end end
-- link a solar neutron activator RTU session -- link a solar neutron activator RTU session
@ -485,12 +511,19 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- 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) ---@return boolean linked environment detector accepted
function public.add_envd(envd) function public.add_envd(envd)
if #self.envd == 0 then local fail_code, fail_str = svsessions.check_rtu_id(envd, self.envd, 99)
local ok = fail_code == RTU_ID_FAIL.OK
if ok then
table.insert(self.envd, envd) table.insert(self.envd, envd)
return true log.debug(util.c(log_tag, "linked environment detector #", envd.get_device_idx(), " [", envd.get_unit_id(), "@", envd.get_session_id(), "]"))
else return false end else
log.warning(util.c(log_tag, "rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")"))
end
return ok
end end
-- purge devices associated with the given RTU session ID -- purge devices associated with the given RTU session ID
@ -512,7 +545,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
self.db.control.br100 = 0 self.db.control.br100 = 0
end end
-- unlink RTU unit sessions if they are closed -- unlink RTU sessions if they are closed
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
-- update degraded state for auto control -- update degraded state for auto control
@ -547,7 +580,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- stop idling when completed -- stop idling when completed
if self.auto_idling and (((util.time_ms() - self.auto_idle_start) > IDLE_TIME) or not self.auto_idle) then if self.auto_idling and (((util.time_ms() - self.auto_idle_start) > IDLE_TIME) or not self.auto_idle) then
log.info(util.c("UNIT ", self.r_id, ": completed idling period")) log.info(util.c(log_tag, "completed idling period"))
self.auto_idling = false self.auto_idling = false
self.plc_i.auto_set_burn(0, false) self.plc_i.auto_set_burn(0, false)
end end
@ -584,7 +617,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
function public.auto_engage() function public.auto_engage()
self.auto_engaged = true self.auto_engaged = true
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": engaged auto control")) log.debug(util.c(log_tag, "engaged auto control"))
self.plc_i.auto_lock(true) self.plc_i.auto_lock(true)
end end
end end
@ -593,7 +626,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
function public.auto_disengage() function public.auto_disengage()
self.auto_engaged = false self.auto_engaged = false
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": disengaged auto control")) log.debug(util.c(log_tag, "disengaged auto control"))
self.plc_i.auto_lock(false) self.plc_i.auto_lock(false)
self.db.control.br100 = 0 self.db.control.br100 = 0
end end
@ -610,7 +643,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
end end
if idle ~= self.auto_idle then if idle ~= self.auto_idle then
log.debug(util.c("UNIT ", self.r_id, ": idling mode changed to ", idle)) log.debug(util.c(log_tag, "idling mode changed to ", idle))
end end
self.auto_idle = idle self.auto_idle = idle
@ -623,7 +656,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
function public.auto_get_effective_limit() function public.auto_get_effective_limit()
local ctrl = self.db.control local ctrl = self.db.control
if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then if (not ctrl.ready) or ctrl.degraded or self.plc_cache.rps_trip then
-- log.debug(util.c("UNIT ", self.r_id, ": effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]")) -- log.debug(util.c(log_tag, "effective limit is zero! ready[", ctrl.ready, "] degraded[", ctrl.degraded, "] rps_trip[", self.plc_cache.rps_trip, "]"))
ctrl.br100 = 0 ctrl.br100 = 0
return 0 return 0
else return ctrl.lim_br100 end else return ctrl.lim_br100 end
@ -634,7 +667,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
function public.auto_commit_br100(ramp) function public.auto_commit_br100(ramp)
if self.auto_engaged then if self.auto_engaged then
if self.plc_i ~= nil then if self.plc_i ~= nil then
log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp)) log.debug(util.c(log_tag, "commit br100 of ", self.db.control.br100, " with ramp set to ", ramp))
local rate = self.db.control.br100 / 100 local rate = self.db.control.br100 / 100
@ -643,16 +676,16 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
if self.auto_idle_start == 0 then if self.auto_idle_start == 0 then
self.auto_idling = true self.auto_idling = true
self.auto_idle_start = util.time_ms() self.auto_idle_start = util.time_ms()
log.info(util.c("UNIT ", self.r_id, ": started idling at ", IDLE_RATE, " mB/t")) log.info(util.c(log_tag, "started idling at ", IDLE_RATE, " mB/t"))
rate = IDLE_RATE rate = IDLE_RATE
elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then
if self.auto_idling then if self.auto_idling then
self.auto_idling = false self.auto_idling = false
log.info(util.c("UNIT ", self.r_id, ": completed idling period")) log.info(util.c(log_tag, "completed idling period"))
end end
else else
log.debug(util.c("UNIT ", self.r_id, ": continuing idle at ", IDLE_RATE, " mB/t")) log.debug(util.c(log_tag, "continuing idle at ", IDLE_RATE, " mB/t"))
rate = IDLE_RATE rate = IDLE_RATE
end end
@ -891,6 +924,29 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
return rate or 0 return rate or 0
end end
-- check which RTUs are connected
---@nodiscard
function public.check_rtu_conns()
local conns = {}
conns.boilers = {}
for i = 1, #self.boilers do
conns.boilers[self.boilers[i].get_device_idx()] = true
end
conns.turbines = {}
for i = 1, #self.turbines do
conns.turbines[self.turbines[i].get_device_idx()] = true
end
conns.tanks = {}
for i = 1, #self.tanks do
conns.tanks[self.tanks[i].get_device_idx()] = true
end
return conns
end
-- get RTU statuses -- get RTU statuses
---@nodiscard ---@nodiscard
function public.get_rtu_statuses() function public.get_rtu_statuses()

View File

@ -84,7 +84,7 @@ function logic.update_annunciator(self)
self.turbine_flow_stable = false self.turbine_flow_stable = false
for t = 1, self.num_turbines do for t = 1, self.num_turbines do
self.turbine_stability_data[t] = { time_state = 0, time_tanks = 0, rotation = 1 } self.turbine_stability_data[t] = { time_state = 0, time_tanks = 0, rotation = 1, input_rate = 0 }
end end
end end
@ -317,7 +317,7 @@ function logic.update_annunciator(self)
local last = self.turbine_stability_data[i] local last = self.turbine_stability_data[i]
if (not self.turbine_flow_stable) and (turbine.state.steam_input_rate > 0) then if not self.turbine_flow_stable then
local rotation = util.turbine_rotation(turbine) local rotation = util.turbine_rotation(turbine)
local rotation_stable = false local rotation_stable = false
@ -351,13 +351,18 @@ function logic.update_annunciator(self)
end end
turbines_stable = turbines_stable and (rotation_stable or flow_stable) turbines_stable = turbines_stable and (rotation_stable or flow_stable)
else elseif math.abs(turbine.state.steam_input_rate - last.input_rate) > 1 then
-- reset to unstable to re-check
last.time_state = 0 last.time_state = 0
last.time_tanks = 0 last.time_tanks = 0
last.rotation = 1 last.rotation = 1
turbines_stable = false turbines_stable = false
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reset stability (new rate ", turbine.state.steam_input_rate, " != ", last.input_rate," mB/t)"))
end end
last.input_rate = turbine.state.steam_input_rate
end end
self.turbine_flow_stable = self.turbine_flow_stable or turbines_stable self.turbine_flow_stable = self.turbine_flow_stable or turbines_stable