mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
commit
07406ca5fc
84
ccmsi.lua
84
ccmsi.lua
@ -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.
|
||||
]]--
|
||||
|
||||
local function println(message) print(tostring(message)) end
|
||||
local function print(message) term.write(tostring(message)) end
|
||||
|
||||
local CCMSI_VERSION = "v1.16"
|
||||
local CCMSI_VERSION = "v1.17"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
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 mode, app, target
|
||||
local install_manifest = manifest_path.."main/install_manifest.json"
|
||||
@ -219,10 +267,27 @@ end
|
||||
|
||||
-- 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
|
||||
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>")
|
||||
lgray()
|
||||
println(" check - check latest versions available")
|
||||
@ -241,6 +306,7 @@ if #opts == 0 or opts[1] == "help" then
|
||||
println(" installer - ccmsi installer (update only)")
|
||||
white();println("<branch>")
|
||||
lgray();println(" main (default) | devel");white()
|
||||
end
|
||||
return
|
||||
else
|
||||
mode = get_opt(opts[1], { "check", "install", "update", "uninstall" })
|
||||
@ -286,20 +352,22 @@ if mode == "check" then
|
||||
-- list all versions
|
||||
for key, value in pairs(manifest.versions) do
|
||||
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 _is_pkt_env then println(tag) end
|
||||
blue();print(local_manifest.versions[key])
|
||||
if value ~= local_manifest.versions[key] then
|
||||
white();print(" (")
|
||||
cyan();print(value);white();println(" available)")
|
||||
else green();println(" (up to date)") end
|
||||
else
|
||||
elseif not _is_pkt_env then
|
||||
lgray();print("not installed");white();print(" (latest ")
|
||||
cyan();print(value);white();println(")")
|
||||
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()
|
||||
end
|
||||
elseif mode == "install" or mode == "update" then
|
||||
|
@ -959,7 +959,10 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
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
|
||||
load_settings(settings_cfg, true)
|
||||
|
@ -520,7 +520,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
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
|
||||
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?")
|
||||
|
@ -89,6 +89,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
|
||||
num_units = conf.num_units,
|
||||
tank_mode = conf.cooling.fac_tank_mode,
|
||||
tank_defs = conf.cooling.fac_tank_defs,
|
||||
tank_list = conf.cooling.fac_tank_list,
|
||||
all_sys_ok = false,
|
||||
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_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
|
||||
for i = 1, #io.facility.tank_list do
|
||||
if io.facility.tank_list[i] == 2 then
|
||||
|
@ -106,7 +106,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
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
|
||||
else
|
||||
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
|
||||
-- close the session
|
||||
_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
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
|
@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local COORDINATOR_VERSION = "v1.5.2"
|
||||
local COORDINATOR_VERSION = "v1.5.6"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
|
@ -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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: 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"}
|
||||
|
||||
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)
|
||||
|
@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.3.1"
|
||||
core.version = "2.3.3"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
@ -2,6 +2,7 @@
|
||||
-- Generic Graphics Element
|
||||
--
|
||||
|
||||
-- local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
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
|
||||
-- remove self from parent
|
||||
-- log.debug("removing " .. self.id .. " from parent")
|
||||
args.parent.__remove_child(self.id)
|
||||
else
|
||||
-- log.debug("no parent for " .. self.id .. " on delete attempt")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
-- Scroll-able List Box Display Graphics Element
|
||||
|
||||
-- local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@ -152,6 +153,7 @@ local function listbox(args)
|
||||
next_y = next_y + item.h + item_pad
|
||||
item.e.reposition(1, item.y)
|
||||
item.e.show()
|
||||
-- log.debug("iterated " .. item.e.get_id())
|
||||
end
|
||||
|
||||
content_height = next_y
|
||||
@ -210,6 +212,7 @@ local function listbox(args)
|
||||
---@param child graphics_element child element
|
||||
function e.on_added(id, child)
|
||||
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
||||
-- log.debug("added child " .. id .. " into slot " .. #list)
|
||||
update_positions()
|
||||
end
|
||||
|
||||
@ -219,10 +222,12 @@ local function listbox(args)
|
||||
for idx, elem in ipairs(list) do
|
||||
if elem.id == id then
|
||||
table.remove(list, idx)
|
||||
-- log.debug("removed child " .. id .. " from slot " .. idx)
|
||||
update_positions()
|
||||
return
|
||||
end
|
||||
end
|
||||
-- log.debug("failed to remove child " .. id)
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
|
@ -57,6 +57,9 @@ local function textbox(args)
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
|
||||
-- trim leading/trailing whitespace
|
||||
lines[i] = util.trim(lines[i])
|
||||
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
|
@ -379,7 +379,10 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
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
|
||||
load_settings(settings_cfg, true)
|
||||
|
@ -376,6 +376,12 @@ function iocontrol.report_link_state(state, sv_addr, api_addr)
|
||||
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)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_svr_tt(trip_time)
|
||||
|
@ -610,7 +610,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
if self.api.r_seq_num == nil then
|
||||
self.api.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
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
|
||||
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 ..
|
||||
@ -686,6 +686,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
self.api.linked = true
|
||||
self.api.addr = src_addr
|
||||
|
||||
iocontrol.report_crd_link_error("")
|
||||
|
||||
if self.sv.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, nil, self.api.addr)
|
||||
else
|
||||
@ -697,24 +699,29 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
else
|
||||
log.debug("received coordinator establish allow without facility configuration")
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
self.sv.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
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
|
||||
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 ..
|
||||
@ -826,25 +833,33 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
|
||||
self.sv.linked = true
|
||||
self.sv.addr = src_addr
|
||||
|
||||
iocontrol.report_svr_link_error("")
|
||||
|
||||
if self.api.linked then
|
||||
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, nil)
|
||||
else
|
||||
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY, self.sv.addr, nil)
|
||||
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
|
||||
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
|
||||
|
||||
self.sv.last_est_ack = est_ack
|
||||
|
@ -20,7 +20,7 @@ local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
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_ts = util.println_ts
|
||||
|
@ -144,7 +144,7 @@ local function new_view(root)
|
||||
for idx = 1, #s_results[tier] do
|
||||
local entry = s_results[tier][idx]
|
||||
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
|
||||
end
|
||||
|
@ -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}
|
||||
|
||||
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
|
||||
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()
|
||||
TextBox{parent=nt_div,x=2,text="Supervisor Address",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local sv = TextBox{parent=nt_div,x=2,text="",alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text="Supervisor Address",fg_bg=label}
|
||||
local sv = TextBox{parent=nt_div,x=2,text=""}
|
||||
|
||||
nt_div.line_break()
|
||||
TextBox{parent=nt_div,x=2,text="Coordinator Address",alignment=ALIGN.LEFT,fg_bg=label}
|
||||
local coord = TextBox{parent=nt_div,x=2,text="",alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text="Coordinator Address",fg_bg=label}
|
||||
local coord = TextBox{parent=nt_div,x=2,text=""}
|
||||
|
||||
sv.register(db.ps, "sv_addr", sv.set_value)
|
||||
coord.register(db.ps, "api_addr", coord.set_value)
|
||||
|
||||
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")
|
||||
TextBox{parent=nt_div,x=2,text=auth,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=nt_div,x=2,text=auth}
|
||||
|
||||
--#endregion
|
||||
|
||||
@ -96,28 +96,28 @@ local function create_pages(root)
|
||||
|
||||
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=db.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Pocket Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version}
|
||||
|
||||
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,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Comms Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.version}
|
||||
|
||||
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=comms.api_version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="API Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.api_version}
|
||||
|
||||
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=util.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Common Lib Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=util.version}
|
||||
|
||||
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=core.version,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Graphics Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=core.version}
|
||||
|
||||
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,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=fw_list,x=2,text="Lockbox Version",fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=lockbox.version}
|
||||
|
||||
--#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}
|
||||
|
||||
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=_VERSION,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=hw_div,x=2,text="Lua Version",fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_VERSION}
|
||||
|
||||
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=_HOST,height=6,alignment=ALIGN.LEFT}
|
||||
TextBox{parent=hw_div,x=2,text="Environment",fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_HOST,height=6}
|
||||
|
||||
--#endregion
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
-- Connection Waiting Spinner
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
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}
|
||||
|
||||
-- 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 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
|
||||
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
|
||||
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
|
||||
|
||||
return root
|
||||
|
@ -42,7 +42,7 @@ local function init(main)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- 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 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)}
|
||||
|
||||
|
@ -55,7 +55,7 @@ return function (data, base_page, title, items, scroll_height)
|
||||
doc_map[item.key] = 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
|
||||
end
|
||||
|
@ -34,9 +34,20 @@ local self = {
|
||||
settings = nil, ---@type plc_config
|
||||
|
||||
run_test_btn = nil, ---@type graphics_element
|
||||
sc_log = nil, ---@type graphics_element
|
||||
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
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
@ -67,6 +78,7 @@ local function handle_packet(packet)
|
||||
self.self_check_msg(nil, true, "")
|
||||
self.sv_addr = packet.scada_frame.src_addr()
|
||||
send_sv(MGMT_TYPE.CLOSE, {})
|
||||
if self.self_check_pass then check_complete() end
|
||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||
error_msg = "error: supervisor connection denied"
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
@ -100,11 +112,10 @@ local function handle_timeout()
|
||||
end
|
||||
|
||||
-- execute the self-check
|
||||
---@param sc_log graphics_element
|
||||
local function self_check(sc_log)
|
||||
local function self_check()
|
||||
self.run_test_btn.disable()
|
||||
|
||||
sc_log.remove_all()
|
||||
self.sc_log.remove_all()
|
||||
ppm.mount_all()
|
||||
|
||||
self.self_check_pass = true
|
||||
@ -143,27 +154,18 @@ local function self_check(sc_log)
|
||||
|
||||
tcd.dispatch_unique(8, handle_timeout)
|
||||
else
|
||||
if self.self_check_pass then check_complete() end
|
||||
self.run_test_btn.enable()
|
||||
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
|
||||
|
||||
-- exit self check back home
|
||||
---@param sc_log 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)
|
||||
self.net_listen = false
|
||||
self.run_test_btn.enable()
|
||||
sc_log.remove_all()
|
||||
self.sc_log.remove_all()
|
||||
main_pane.set_value(1)
|
||||
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}
|
||||
|
||||
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 }
|
||||
|
||||
function self.self_check_msg(msg, success, fail_msg)
|
||||
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}
|
||||
last_check[2] = e.get_x()+string.len(msg)
|
||||
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))}
|
||||
|
||||
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)}
|
||||
end
|
||||
|
||||
@ -210,8 +212,8 @@ function check.create(main_pane, settings_cfg, check_sys, style)
|
||||
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}
|
||||
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}
|
||||
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()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
|
||||
end
|
||||
|
||||
-- handle incoming modem messages
|
||||
|
@ -450,7 +450,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
|
||||
end
|
||||
|
||||
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
|
||||
load_settings(settings_cfg, true)
|
||||
|
@ -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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: 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"}
|
||||
|
||||
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)
|
||||
|
@ -833,7 +833,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
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
|
||||
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 ..
|
||||
|
@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
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_ts = util.println_ts
|
||||
|
@ -650,8 +650,11 @@ local function config_view(display)
|
||||
|
||||
---@param exclude_conns boolean? true to exclude saving peripheral/redstone connections
|
||||
local function save_and_continue(exclude_conns)
|
||||
for k, v in pairs(tmp_cfg) do
|
||||
if not (exclude_conns and (k == "Peripherals" or k == "Redstone")) then settings.set(k, v) end
|
||||
for _, field in ipairs(fields) do
|
||||
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
|
||||
|
||||
-- always set these if missing
|
||||
|
@ -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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: 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"}
|
||||
|
||||
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)
|
||||
|
@ -444,7 +444,7 @@ function rtu.comms(version, nic, conn_watchdog)
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num() + 1
|
||||
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
|
||||
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 ..
|
||||
|
@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "v1.10.3"
|
||||
local RTU_VERSION = "v1.10.6"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
|
@ -5,7 +5,7 @@
|
||||
---@class types
|
||||
local types = {}
|
||||
|
||||
-- CLASSES --
|
||||
--#region CLASSES
|
||||
|
||||
---@class tank_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 rsio table|nil
|
||||
|
||||
--#endregion
|
||||
|
||||
-- ALIASES --
|
||||
|
||||
---@alias color integer
|
||||
|
||||
-- ENUMERATION TYPES --
|
||||
--#region
|
||||
--#region ENUMERATION TYPES
|
||||
|
||||
---@enum TEMP_SCALE
|
||||
types.TEMP_SCALE = {
|
||||
@ -169,6 +170,15 @@ function types.rtu_type_to_string(utype)
|
||||
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
|
||||
types.TRI_FAIL = {
|
||||
OK = 1,
|
||||
@ -290,8 +300,7 @@ types.ALARM_STATE_NAMES = {
|
||||
|
||||
--#endregion
|
||||
|
||||
-- STRING TYPES --
|
||||
--#region
|
||||
--#region STRING TYPES
|
||||
|
||||
---@alias side
|
||||
---|"top"
|
||||
@ -405,8 +414,7 @@ types.DUMPING_MODE = {
|
||||
|
||||
--#endregion
|
||||
|
||||
-- MODBUS --
|
||||
--#region
|
||||
--#region MODBUS
|
||||
|
||||
-- MODBUS function codes
|
||||
---@enum MODBUS_FCODE
|
||||
|
@ -24,7 +24,7 @@ local t_pack = table.pack
|
||||
local util = {}
|
||||
|
||||
-- scada-common version
|
||||
util.version = "1.4.2"
|
||||
util.version = "1.4.3"
|
||||
|
||||
util.TICK_TIME_S = 0.05
|
||||
util.TICK_TIME_MS = 50
|
||||
@ -110,6 +110,15 @@ function util.pad(str, n)
|
||||
return t_concat{util.spaces(lpad), str, util.spaces(rpad)}
|
||||
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
|
||||
---@nodiscard
|
||||
---@param str string
|
||||
|
@ -907,7 +907,10 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
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
|
||||
load_settings(settings_cfg, true)
|
||||
|
File diff suppressed because it is too large
Load Diff
832
supervisor/facility_update.lua
Normal file
832
supervisor/facility_update.lua
Normal 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
|
47
supervisor/panel/components/chk_entry.lua
Normal file
47
supervisor/panel/components/chk_entry.lua
Normal 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
|
@ -10,6 +10,7 @@ local supervisor = require("supervisor.supervisor")
|
||||
local pgi = require("supervisor.panel.pgi")
|
||||
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 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 fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: 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"}
|
||||
|
||||
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)
|
||||
@ -83,7 +84,7 @@ local function init(panel)
|
||||
-- page handling
|
||||
--
|
||||
|
||||
-- plc page
|
||||
-- plc sessions page
|
||||
|
||||
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}
|
||||
@ -115,13 +116,13 @@ local function init(panel)
|
||||
plc_list.line_break()
|
||||
end
|
||||
|
||||
-- rtu page
|
||||
-- rtu sessions page
|
||||
|
||||
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 _ = 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_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_color", crd_rtt.recolor)
|
||||
|
||||
-- pocket diagnostics page
|
||||
-- pocket sessions page
|
||||
|
||||
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 _ = 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
|
||||
|
||||
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}
|
||||
|
||||
@ -161,12 +184,14 @@ local function init(panel)
|
||||
{ name = "RTU", color = style.fp.text },
|
||||
{ name = "CRD", 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
|
||||
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
|
||||
-- link RTU/PDG/CHK list management to PGI
|
||||
pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry, chk_list, chk_entry)
|
||||
end
|
||||
|
||||
return init
|
||||
|
@ -10,10 +10,12 @@ local pgi = {}
|
||||
local data = {
|
||||
rtu_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
|
||||
pdg_entry = nil, ---@type function
|
||||
-- session entries
|
||||
s_entries = { rtu = {}, pdg = {} }
|
||||
chk_entry = nil, ---@type function
|
||||
-- list entries
|
||||
entries = { rtu = {}, pdg = {}, chk = {}, missing = {} }
|
||||
}
|
||||
|
||||
-- link list boxes
|
||||
@ -21,19 +23,25 @@ local data = {
|
||||
---@param rtu_entry function RTU entry constructor
|
||||
---@param pdg_list graphics_element pocket diagnostics list element
|
||||
---@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.pdg_list = pdg_list
|
||||
data.chk_list = chk_list
|
||||
data.rtu_entry = rtu_entry
|
||||
data.pdg_entry = pdg_entry
|
||||
data.chk_entry = chk_entry
|
||||
end
|
||||
|
||||
-- unlink all fields, disabling the PGI
|
||||
function pgi.unlink()
|
||||
data.rtu_list = nil
|
||||
data.pdg_list = nil
|
||||
data.chk_list = nil
|
||||
data.rtu_entry = nil
|
||||
data.pdg_entry = nil
|
||||
data.chk_entry = nil
|
||||
end
|
||||
|
||||
-- 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)
|
||||
|
||||
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
|
||||
log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
|
||||
end
|
||||
@ -53,15 +62,17 @@ end
|
||||
-- delete an RTU entry from the RTU list
|
||||
---@param session_id integer RTU session
|
||||
function pgi.delete_rtu_entry(session_id)
|
||||
if data.s_entries.rtu[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.rtu[session_id].delete)
|
||||
data.s_entries.rtu[session_id] = nil
|
||||
if data.entries.rtu[session_id] ~= nil then
|
||||
local success, result = pcall(data.entries.rtu[session_id].delete)
|
||||
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)
|
||||
end
|
||||
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
|
||||
|
||||
@ -72,7 +83,8 @@ function pgi.create_pdg_entry(session_id)
|
||||
local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
|
||||
|
||||
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
|
||||
log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
|
||||
end
|
||||
@ -82,15 +94,92 @@ end
|
||||
-- delete a PDG entry from the PDG list
|
||||
---@param session_id integer pocket diagnostics session
|
||||
function pgi.delete_pdg_entry(session_id)
|
||||
if data.s_entries.pdg[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.pdg[session_id].delete)
|
||||
data.s_entries.pdg[session_id] = nil
|
||||
if data.entries.pdg[session_id] ~= nil then
|
||||
local success, result = pcall(data.entries.pdg[session_id].delete)
|
||||
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)
|
||||
end
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
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 = {
|
||||
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)
|
||||
-- check sequence number
|
||||
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
|
||||
else
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
-- 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)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_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
|
||||
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
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@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)) })
|
||||
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
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
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
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_waste_product(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set waste mode packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN set waste mode packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
if pkt.length == 2 then
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_pu_fallback(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set pu fallback packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN set pu fallback packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
if pkt.length == 2 then
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
|
||||
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
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command unknown")
|
||||
log.debug(log_tag .. "CRDN facility command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN facility command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == CRDN_TYPE.UNIT_BUILDS then
|
||||
-- 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
|
||||
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command burn rate missing option")
|
||||
log.debug(log_tag .. "CRDN unit command burn rate missing option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
|
||||
unit.set_waste_mode(pkt.data[3])
|
||||
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
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
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
|
||||
unit.ack_alarm(pkt.data[3])
|
||||
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
|
||||
elseif cmd == UNIT_COMMAND.RESET_ALARM then
|
||||
if pkt.length == 3 then
|
||||
unit.reset_alarm(pkt.data[3])
|
||||
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
|
||||
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
|
||||
facility.set_group(unit.get_id(), pkt.data[3])
|
||||
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
|
||||
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
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command unknown")
|
||||
log.debug(log_tag .. "CRDN unit command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command invalid")
|
||||
log.debug(log_tag .. "CRDN unit command invalid")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command packet length mismatch")
|
||||
log.debug(log_tag .. "CRDN unit command packet length mismatch")
|
||||
end
|
||||
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
|
||||
@ -370,7 +374,7 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
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
|
||||
|
||||
-- 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) })
|
||||
end
|
||||
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
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
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
|
||||
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
|
||||
if not self.connected then
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -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
|
||||
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 = {
|
||||
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_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))
|
||||
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])
|
||||
self.received_status_cache = true
|
||||
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
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet invalid")
|
||||
log.debug(log_tag .. "RPLC status packet invalid")
|
||||
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
|
||||
return pkt.data[1]
|
||||
else
|
||||
log.debug(log_header .. "RPLC ACK length mismatch")
|
||||
log.debug(log_tag .. "RPLC ACK length mismatch")
|
||||
return nil
|
||||
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)
|
||||
-- check sequence number
|
||||
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
|
||||
else
|
||||
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
|
||||
-- check reactor ID
|
||||
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
|
||||
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
|
||||
_handle_status(pkt)
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC status packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
|
||||
-- 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
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
|
||||
else
|
||||
log.debug(log_header .. "RPLC struct packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC struct packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
|
||||
-- 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
|
||||
self.acks.burn_rate = true
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "burn rate update failed!")
|
||||
log.debug(log_tag .. "burn rate update failed!")
|
||||
end
|
||||
|
||||
-- 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
|
||||
self.sDB.control_state = true
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "enable failed!")
|
||||
log.debug(log_tag .. "enable failed!")
|
||||
end
|
||||
|
||||
-- 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.sDB.control_state = false
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "disable failed!")
|
||||
log.debug(log_tag .. "disable failed!")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
|
||||
-- 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.sDB.control_state = false
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "manual SCRAM failed!")
|
||||
log.debug(log_tag .. "manual SCRAM failed!")
|
||||
end
|
||||
|
||||
-- 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.sDB.control_state = false
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. " automatic SCRAM failed!")
|
||||
log.debug(log_tag .. " automatic SCRAM failed!")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_STATUS then
|
||||
-- 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
|
||||
else
|
||||
-- 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
|
||||
else
|
||||
log.debug(log_header .. "RPLC RPS status packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC RPS status packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_ALARM then
|
||||
-- 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
|
||||
else
|
||||
-- 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
|
||||
else
|
||||
log.debug(log_header .. "RPLC RPS alarm packet length mismatch")
|
||||
log.debug(log_tag .. "RPLC RPS alarm packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.RPS_RESET then
|
||||
-- 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_trip_cause = "ok"
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. "RPS reset failed")
|
||||
log.debug(log_tag .. "RPS reset failed")
|
||||
end
|
||||
|
||||
-- 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
|
||||
local ack = _get_ack(pkt)
|
||||
if not ack then
|
||||
log.debug(log_header .. "RPS auto reset failed")
|
||||
log.debug(log_tag .. "RPS auto reset failed")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE 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
|
||||
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
|
||||
self.acks.burn_rate = true
|
||||
else
|
||||
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
|
||||
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
|
||||
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
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@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
|
||||
|
||||
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
|
||||
|
||||
-- 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)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_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
|
||||
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
|
||||
@ -639,7 +643,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
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
|
||||
|
||||
-- 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, {})
|
||||
end
|
||||
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
|
||||
elseif message.qtype == mqueue.TYPE.DATA then
|
||||
-- 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
|
||||
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
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
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
|
||||
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
|
||||
if not self.connected then
|
||||
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
|
||||
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 rtimes.status_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPE.MEK_STATUS, {})
|
||||
_send(RPLC_TYPE.STATUS, {})
|
||||
rtimes.status_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
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 = {
|
||||
-- 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)
|
||||
-- check sequence number
|
||||
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
|
||||
else
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
-- 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)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_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
|
||||
-- get the state of 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])
|
||||
_send_mgmt(MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
|
||||
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
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag tone set packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA diag tone set packet length mismatch")
|
||||
end
|
||||
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
|
||||
|
||||
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])
|
||||
_send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
|
||||
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
|
||||
else
|
||||
log.debug(log_header .. "SCADA diag alarm set packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA diag alarm set packet length mismatch")
|
||||
end
|
||||
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
|
||||
|
||||
if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end
|
||||
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
|
||||
@ -205,7 +209,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
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
|
||||
|
||||
-- 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
|
||||
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
|
||||
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
|
||||
if not self.connected then
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -30,7 +30,7 @@ local PERIODICS = {
|
||||
ALARM_TONES = 500
|
||||
}
|
||||
|
||||
-- create a new RTU session
|
||||
-- create a new RTU gateway session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param s_addr integer device source address
|
||||
@ -38,14 +38,14 @@ local PERIODICS = {
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@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 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)
|
||||
-- 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 log_header = "rtu_session(" .. id .. "): "
|
||||
local log_tag = "rtu_gw_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
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
|
||||
-- validation fail
|
||||
log.debug(log_header .. "_handle_advertisement(): advertisement unit validation failure")
|
||||
log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
|
||||
else
|
||||
if unit_advert.reactor > 0 then
|
||||
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
|
||||
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
-- 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
|
||||
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
|
||||
else
|
||||
-- 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
|
||||
elseif u_type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
-- 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
|
||||
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
|
||||
@ -195,20 +195,20 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
|
||||
self.units[i] = unit
|
||||
unit_count = unit_count + 1
|
||||
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
|
||||
|
||||
databus.tx_rtu_units(id, unit_count)
|
||||
end
|
||||
|
||||
-- mark this RTU session as closed, stop watchdog
|
||||
-- mark this RTU gateway session as closed, stop watchdog
|
||||
local function _close()
|
||||
self.conn_watchdog.cancel()
|
||||
self.connected = false
|
||||
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
|
||||
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)
|
||||
-- check sequence number
|
||||
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
|
||||
else
|
||||
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
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
-- local rtu_send = pkt.data[2]
|
||||
-- local rtu_gw_send = pkt.data[2]
|
||||
local srv_now = util.time()
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
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
|
||||
|
||||
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
|
||||
-- log.debug(log_tag .. "RTU GW RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_tag .. "RTU GW TT = " .. (srv_now - rtu_gw_send) .. "ms")
|
||||
|
||||
databus.tx_rtu_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_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
|
||||
-- RTU unit advertisement
|
||||
log.debug(log_header .. "received updated advertisement")
|
||||
-- RTU advertisement
|
||||
log.debug(log_tag .. "received updated advertisement")
|
||||
self.advert = pkt.data
|
||||
|
||||
-- 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()
|
||||
end
|
||||
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
|
||||
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
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- get the session ID
|
||||
-- get the gateway session ID
|
||||
function public.get_id() return id end
|
||||
|
||||
-- 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()
|
||||
_close()
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
println(log_header .. "connection to RTU closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
println(log_tag .. "connection to RTU GW closed by server")
|
||||
log.info(log_tag .. "session closed by server")
|
||||
end
|
||||
|
||||
-- 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
|
||||
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
|
||||
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
|
||||
if not self.connected then
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -32,10 +32,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new boilerv rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@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)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
|
@ -44,10 +44,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new dynamicv rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@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)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
||||
|
@ -23,10 +23,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new environment detector rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer
|
||||
---@param unit_id integer
|
||||
---@param advert rtu_advertisement
|
||||
---@param out_queue mqueue
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function envd.new(session_id, unit_id, advert, out_queue)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
|
@ -32,10 +32,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new imatrix rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@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)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
|
||||
|
@ -45,10 +45,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new redstone rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer
|
||||
---@param unit_id integer
|
||||
---@param advert rtu_advertisement
|
||||
---@param out_queue mqueue
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU message out queue
|
||||
function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
|
||||
|
@ -29,10 +29,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new sna rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@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)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.SNA then
|
||||
|
@ -32,10 +32,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new sps rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@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)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPE.SPS then
|
||||
|
@ -44,10 +44,10 @@ local PERIODICS = {
|
||||
|
||||
-- create a new turbinev rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer RTU ID
|
||||
---@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)
|
||||
-- checks
|
||||
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
|
@ -24,7 +24,7 @@ unit_session.RTU_US_DATA = RTU_US_DATA
|
||||
|
||||
-- create a new unit session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param session_id integer RTU gateway session ID
|
||||
---@param unit_id integer MODBUS unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement for this unit
|
||||
---@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 --
|
||||
|
||||
-- get the unit ID
|
||||
-- get the RTU gateway session ID
|
||||
---@nodiscard
|
||||
function public.get_session_id() return session_id end
|
||||
-- get the unit ID
|
||||
---@nodiscard
|
||||
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
|
||||
---@nodiscard
|
||||
function public.get_device_idx() return self.device_index or 0 end
|
||||
|
@ -1,9 +1,15 @@
|
||||
--
|
||||
-- Supervisor Sessions Handler
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local databus = require("supervisor.databus")
|
||||
local facility = require("supervisor.facility")
|
||||
|
||||
local pgi = require("supervisor.panel.pgi")
|
||||
|
||||
local coordinator = require("supervisor.session.coordinator")
|
||||
local plc = require("supervisor.session.plc")
|
||||
@ -11,13 +17,15 @@ local pocket = require("supervisor.session.pocket")
|
||||
local rtu = require("supervisor.session.rtu")
|
||||
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_DATA = plc.PLC_S_DATA
|
||||
local CRD_S_DATA = coordinator.CRD_S_DATA
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
local PLC_S_DATA = plc.PLC_S_DATA
|
||||
|
||||
local CRD_S_DATA = coordinator.CRD_S_DATA
|
||||
|
||||
local svsessions = {}
|
||||
|
||||
@ -37,12 +45,13 @@ local self = {
|
||||
config = nil, ---@type svr_config
|
||||
facility = nil, ---@type facility|nil
|
||||
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
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
--#region PRIVATE FUNCTIONS
|
||||
|
||||
-- handle a session output queue
|
||||
---@param session sv_session_structs
|
||||
@ -190,18 +199,184 @@ local function _find_session(list, s_addr)
|
||||
return nil
|
||||
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
|
||||
---@param nic nic network interface device
|
||||
---@param fp_ok boolean front panel active
|
||||
---@param config svr_config supervisor configuration
|
||||
---@param cooling_conf sv_cooling_conf cooling configuration definition
|
||||
function svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||
---@param facility facility
|
||||
function svsessions.init(nic, fp_ok, config, facility)
|
||||
self.nic = nic
|
||||
self.fp_ok = fp_ok
|
||||
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
|
||||
|
||||
-- find an RTU session by the computer ID
|
||||
@ -466,6 +641,9 @@ function svsessions.iterate_all()
|
||||
|
||||
-- iterate units
|
||||
self.facility.update_units()
|
||||
|
||||
-- update tracking of bad RTU IDs and missing devices
|
||||
_update_dev_dbg()
|
||||
end
|
||||
|
||||
-- delete all closed sessions
|
||||
@ -482,4 +660,6 @@ function svsessions.close_all()
|
||||
svsessions.free_all_closed()
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return svsessions
|
||||
|
@ -16,12 +16,13 @@ local core = require("graphics.core")
|
||||
|
||||
local configure = require("supervisor.configure")
|
||||
local databus = require("supervisor.databus")
|
||||
local facility = require("supervisor.facility")
|
||||
local renderer = require("supervisor.renderer")
|
||||
local supervisor = require("supervisor.supervisor")
|
||||
|
||||
local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local SUPERVISOR_VERSION = "v1.4.2"
|
||||
local SUPERVISOR_VERSION = "v1.5.2"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@ -129,9 +130,12 @@ local function main()
|
||||
println_ts = function (_) end
|
||||
end
|
||||
|
||||
-- create facility and unit objects
|
||||
local sv_facility = facility.new(config)
|
||||
|
||||
-- create network interface then setup comms
|
||||
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)
|
||||
local MAIN_CLOCK = 0.15
|
||||
|
@ -102,14 +102,12 @@ end
|
||||
---@param _version string supervisor version
|
||||
---@param nic nic network interface device
|
||||
---@param fp_ok boolean if the front panel UI is running
|
||||
---@param facility facility facility instance
|
||||
---@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
|
||||
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 = {
|
||||
last_est_acks = {}
|
||||
}
|
||||
@ -122,8 +120,8 @@ function supervisor.comms(_version, nic, fp_ok)
|
||||
nic.closeAll()
|
||||
nic.open(config.SVR_Channel)
|
||||
|
||||
-- pass modem, status, and config data to svsessions
|
||||
svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||
-- pass system data and objects to svsessions
|
||||
svsessions.init(nic, fp_ok, config, facility)
|
||||
|
||||
-- send an establish request response
|
||||
---@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"))
|
||||
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
|
||||
if last_ack ~= ESTABLISH_ACK.COLLISION then
|
||||
log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator")
|
||||
|
@ -1,12 +1,13 @@
|
||||
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 log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local logic = require("supervisor.unitlogic")
|
||||
local logic = require("supervisor.unitlogic")
|
||||
|
||||
local plc = require("supervisor.session.plc")
|
||||
local rsctl = require("supervisor.session.rsctl")
|
||||
local plc = require("supervisor.session.plc")
|
||||
local rsctl = require("supervisor.session.rsctl")
|
||||
local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
@ -14,6 +15,7 @@ local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local RTU_ID_FAIL = types.RTU_ID_FAIL
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
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
|
||||
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
|
||||
|
||||
local log_tag = "UNIT " .. reactor_id .. ": "
|
||||
|
||||
---@class _unit_self
|
||||
local self = {
|
||||
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.GeneratorTrip, 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
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
@ -420,6 +424,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
self.plc_s = plc_session
|
||||
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_dt(DT_KEYS.ReactorTemp)
|
||||
_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
|
||||
function public.add_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
|
||||
_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
|
||||
---@return boolean linked turbine accepted to associated device slot
|
||||
function public.add_turbine(turbine)
|
||||
if #self.turbines < num_turbines and turbine.get_device_idx() <= num_turbines then
|
||||
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)
|
||||
log.debug(util.c(log_tag, "linked turbine #", turbine.get_device_idx(), " [", turbine.get_unit_id(), "@", turbine.get_session_id(), "]"))
|
||||
|
||||
-- reset deltas
|
||||
_reset_dt(DT_KEYS.TurbineSteam .. 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
|
||||
else return false end
|
||||
return ok
|
||||
end
|
||||
|
||||
-- link a boiler RTU session
|
||||
---@param boiler unit_session
|
||||
---@return boolean linked boiler accepted to associated device slot
|
||||
function public.add_boiler(boiler)
|
||||
if #self.boilers < num_boilers and boiler.get_device_idx() <= num_boilers then
|
||||
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)
|
||||
log.debug(util.c(log_tag, "linked boiler #", boiler.get_device_idx(), " [", boiler.get_unit_id(), "@", boiler.get_session_id(), "]"))
|
||||
|
||||
-- reset deltas
|
||||
_reset_dt(DT_KEYS.BoilerWater .. 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.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
|
||||
else return false end
|
||||
return ok
|
||||
end
|
||||
|
||||
-- link a dynamic tank RTU session
|
||||
---@param dynamic_tank unit_session
|
||||
---@return boolean linked dynamic tank accepted (max 1)
|
||||
function public.add_tank(dynamic_tank)
|
||||
if #self.tanks == 0 then
|
||||
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)
|
||||
return true
|
||||
else return false end
|
||||
log.debug(util.c(log_tag, "linked dynamic tank [", dynamic_tank.get_unit_id(), "@", dynamic_tank.get_session_id(), "]"))
|
||||
else
|
||||
log.warning(util.c(log_tag, "rejected dynamic tank linking due to failure code ", fail_code, " (", fail_str, ")"))
|
||||
end
|
||||
|
||||
return ok
|
||||
end
|
||||
|
||||
-- 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
|
||||
---@param envd unit_session
|
||||
---@return boolean linked environment detector accepted (max 1)
|
||||
---@return boolean linked environment detector accepted
|
||||
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)
|
||||
return true
|
||||
else return false end
|
||||
log.debug(util.c(log_tag, "linked environment detector #", envd.get_device_idx(), " [", envd.get_unit_id(), "@", envd.get_session_id(), "]"))
|
||||
else
|
||||
log.warning(util.c(log_tag, "rejected environment detector linking due to failure code ", fail_code, " (", fail_str, ")"))
|
||||
end
|
||||
|
||||
return ok
|
||||
end
|
||||
|
||||
-- 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
|
||||
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
|
||||
|
||||
-- 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
|
||||
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.plc_i.auto_set_burn(0, false)
|
||||
end
|
||||
@ -584,7 +617,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
function public.auto_engage()
|
||||
self.auto_engaged = true
|
||||
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)
|
||||
end
|
||||
end
|
||||
@ -593,7 +626,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
function public.auto_disengage()
|
||||
self.auto_engaged = false
|
||||
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.db.control.br100 = 0
|
||||
end
|
||||
@ -610,7 +643,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
local ctrl = self.db.control
|
||||
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
|
||||
return 0
|
||||
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)
|
||||
if self.auto_engaged 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
|
||||
|
||||
@ -643,16 +676,16 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
if self.auto_idle_start == 0 then
|
||||
self.auto_idling = true
|
||||
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
|
||||
elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then
|
||||
if self.auto_idling then
|
||||
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
|
||||
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
|
||||
end
|
||||
@ -891,6 +924,29 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
return rate or 0
|
||||
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
|
||||
---@nodiscard
|
||||
function public.get_rtu_statuses()
|
||||
|
@ -84,7 +84,7 @@ function logic.update_annunciator(self)
|
||||
self.turbine_flow_stable = false
|
||||
|
||||
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
|
||||
|
||||
@ -317,7 +317,7 @@ function logic.update_annunciator(self)
|
||||
|
||||
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_stable = false
|
||||
|
||||
@ -351,13 +351,18 @@ function logic.update_annunciator(self)
|
||||
end
|
||||
|
||||
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_tanks = 0
|
||||
last.rotation = 1
|
||||
|
||||
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
|
||||
|
||||
last.input_rate = turbine.state.steam_input_rate
|
||||
end
|
||||
|
||||
self.turbine_flow_stable = self.turbine_flow_stable or turbines_stable
|
||||
|
Loading…
Reference in New Issue
Block a user