#123 RTU startup without devices, fixed repeat RTU advert handling, added PPM virtual devices, fixed log out of space detection, updated RTU type conversion functions in comms

This commit is contained in:
Mikayla Fischler 2022-11-12 01:35:31 -05:00
parent f940c136bf
commit 1a01bec7e4
18 changed files with 205 additions and 109 deletions

View File

@ -16,7 +16,7 @@ local config = require("coordinator.config")
local coordinator = require("coordinator.coordinator") local coordinator = require("coordinator.coordinator")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local COORDINATOR_VERSION = "alpha-v0.6.9" local COORDINATOR_VERSION = "alpha-v0.6.10"
local print = util.print local print = util.print
local println = util.println local println = util.println

View File

@ -13,7 +13,7 @@ local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc") local plc = require("reactor-plc.plc")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "beta-v0.9.4" local R_PLC_VERSION = "beta-v0.9.5"
local print = util.print local print = util.print
local println = util.println local println = util.println

View File

@ -24,7 +24,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "beta-v0.9.1" local RTU_VERSION = "beta-v0.9.2"
local rtu_t = types.rtu_t local rtu_t = types.rtu_t
@ -257,17 +257,22 @@ local function configure()
local device = ppm.get_periph(name) local device = ppm.get_periph(name)
if device == nil then local type = nil
local message = util.c("configure> '", name, "' not found")
println(message)
log.fatal(message)
return false
else
local type = ppm.get_type(name)
local rtu_iface = nil ---@type rtu_device local rtu_iface = nil ---@type rtu_device
local rtu_type = "" local rtu_type = ""
local formed = nil ---@type boolean|nil local formed = nil ---@type boolean|nil
if device == nil then
local message = util.c("configure> '", name, "' not found, using placeholder")
println(message)
log.warning(message)
-- mount a virtual (placeholder) device
type, device = ppm.mount_virtual()
else
type = ppm.get_type(name)
end
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
rtu_type = rtu_t.boiler_valve rtu_type = rtu_t.boiler_valve
@ -320,6 +325,10 @@ local function configure()
-- advanced peripherals environment detector -- advanced peripherals environment detector
rtu_type = rtu_t.env_detector rtu_type = rtu_t.env_detector
rtu_iface = envd_rtu.new(device) rtu_iface = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device
rtu_type = "virtual"
rtu_iface = rtu.init_unit().interface()
else else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")") local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
println_ts(message) println_ts(message)
@ -327,7 +336,6 @@ local function configure()
return false return false
end end
if rtu_iface ~= nil then
---@class rtu_unit_registry_entry ---@class rtu_unit_registry_entry
local rtu_unit = { local rtu_unit = {
name = name, name = name,
@ -348,8 +356,6 @@ local function configure()
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor)) log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor))
end end
end
end
-- we made it through all that trusting-user-to-write-a-config-file chaos -- we made it through all that trusting-user-to-write-a-config-file chaos
return true return true

View File

@ -122,9 +122,37 @@ function threads.thread__main(smem)
-- find disconnected device to reconnect -- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events -- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then if unit.name == param1 then
local resend_advert = false
-- found, re-link -- found, re-link
unit.device = device unit.device = device
if unit.type == "virtual" then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
unit.type = rtu_t.boiler_valve
elseif type == "turbineValve" then
-- turbine multiblock
unit.type = rtu_t.turbine_valve
elseif type == "inductionPort" then
-- induction matrix multiblock
unit.type = rtu_t.induction_matrix
elseif type == "spsPort" then
-- SPS multiblock
unit.type = rtu_t.sps
elseif type == "solarNeutronActivator" then
-- SNA
unit.type = rtu_t.sna
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
unit.type = rtu_t.env_detector
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
end
if unit.type == rtu_t.boiler_valve then if unit.type == rtu_t.boiler_valve then
unit.rtu = boilerv_rtu.new(device) unit.rtu = boilerv_rtu.new(device)
unit.formed = true unit.formed = true
@ -142,14 +170,19 @@ function threads.thread__main(smem)
elseif unit.type == rtu_t.env_detector then elseif unit.type == rtu_t.env_detector then
unit.rtu = envd_rtu.new(device) unit.rtu = envd_rtu.new(device)
else else
log.error(util.c("unreachable case occured trying to identify reconnected RTU unit type (", unit.name, ")"), true) log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
end end
unit.modbus_io = modbus.new(unit.rtu, true) unit.modbus_io = modbus.new(unit.rtu, true)
rtu_comms.send_remounted(unit.index)
println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name)
log.info("reconnected the " .. unit.type .. " on interface " .. unit.name)
if resend_advert then
rtu_comms.send_advertisement(units)
else
rtu_comms.send_remounted(unit.index)
end
end end
end end
end end
@ -274,7 +307,6 @@ function threads.thread__unit_comms(smem, unit)
local last_update = util.time() local last_update = util.time()
local check_formed = type(unit.formed) == "boolean"
local last_f_check = 0 local last_f_check = 0
local detail_name = util.c(unit.type, " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor) local detail_name = util.c(unit.type, " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor)
@ -308,8 +340,8 @@ function threads.thread__unit_comms(smem, unit)
end end
-- check if multiblocks is still formed -- check if multiblock is still formed if this is a multiblock
if check_formed and (util.time() - last_f_check > 1000) then if (type(unit.formed) == "boolean") and (util.time() - last_f_check > 1000) then
if (not unit.formed) and unit.device.isFormed() then if (not unit.formed) and unit.device.isFormed() then
-- newly re-formed -- newly re-formed
local iface = ppm.get_iface(unit.device) local iface = ppm.get_iface(unit.device)

View File

@ -642,6 +642,12 @@ function comms.rtu_t_to_unit_type(type)
return RTU_UNIT_TYPES.TURBINE_VALVE return RTU_UNIT_TYPES.TURBINE_VALVE
elseif type == rtu_t.induction_matrix then elseif type == rtu_t.induction_matrix then
return RTU_UNIT_TYPES.IMATRIX return RTU_UNIT_TYPES.IMATRIX
elseif type == rtu_t.sps then
return RTU_UNIT_TYPES.SPS
elseif type == rtu_t.sna then
return RTU_UNIT_TYPES.SNA
elseif type == rtu_t.env_detector then
return RTU_UNIT_TYPES.ENV_DETECTOR
end end
return nil return nil
@ -659,6 +665,12 @@ function comms.advert_type_to_rtu_t(utype)
return rtu_t.turbine_valve return rtu_t.turbine_valve
elseif utype == RTU_UNIT_TYPES.IMATRIX then elseif utype == RTU_UNIT_TYPES.IMATRIX then
return rtu_t.induction_matrix return rtu_t.induction_matrix
elseif utype == RTU_UNIT_TYPES.SPS then
return rtu_t.sps
elseif utype == RTU_UNIT_TYPES.SNA then
return rtu_t.sna
elseif utype == RTU_UNIT_TYPES.ENV_DETECTOR then
return rtu_t.env_detector
end end
return nil return nil

View File

@ -58,6 +58,7 @@ end
-- private log write function -- private log write function
---@param msg string ---@param msg string
local function _log(msg) local function _log(msg)
local out_of_space = false
local time_stamp = os.date("[%c] ") local time_stamp = os.date("[%c] ")
local stamped = time_stamp .. util.strval(msg) local stamped = time_stamp .. util.strval(msg)
@ -69,15 +70,17 @@ local function _log(msg)
-- if we don't have space, we need to create a new log file -- if we don't have space, we need to create a new log file
if not status then if (not status) and (result ~= nil) then
if result == "Out of space" then out_of_space = string.find(result, "Out of space") ~= nil
if out_of_space then
-- will delete log file -- will delete log file
elseif result ~= nil then else
util.println("unknown error writing to logfile: " .. result) util.println("unknown error writing to logfile: " .. result)
end end
end end
if (result == "Out of space") or (free_space(_log_sys.path) < 100) then if out_of_space or (free_space(_log_sys.path) < 100) then
-- delete the old log file and open a new one -- delete the old log file and open a new one
_log_sys.file.close() _log_sys.file.close()
fs.delete(_log_sys.path) fs.delete(_log_sys.path)

View File

@ -12,8 +12,11 @@ local ACCESS_FAULT = nil ---@type nil
local UNDEFINED_FIELD = "undefined field" local UNDEFINED_FIELD = "undefined field"
local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
ppm.ACCESS_FAULT = ACCESS_FAULT ppm.ACCESS_FAULT = ACCESS_FAULT
ppm.UNDEFINED_FIELD = UNDEFINED_FIELD ppm.UNDEFINED_FIELD = UNDEFINED_FIELD
ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
---------------------------- ----------------------------
-- PRIVATE DATA/FUNCTIONS -- -- PRIVATE DATA/FUNCTIONS --
@ -23,6 +26,7 @@ local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local _ppm_sys = { local _ppm_sys = {
mounts = {}, mounts = {},
next_vid = 0,
auto_cf = false, auto_cf = false,
faulted = false, faulted = false,
last_fault = "", last_fault = "",
@ -42,10 +46,15 @@ local function peri_init(iface)
last_fault = "", last_fault = "",
fault_counts = {}, fault_counts = {},
auto_cf = true, auto_cf = true,
type = peripheral.getType(iface), type = VIRTUAL_DEVICE_TYPE,
device = peripheral.wrap(iface) device = {}
} }
if iface ~= "__virtual__" then
self.type = peripheral.getType(iface)
self.device = peripheral.wrap(iface)
end
-- initialization process (re-map) -- initialization process (re-map)
for key, func in pairs(self.device) do for key, func in pairs(self.device) do
@ -245,6 +254,19 @@ function ppm.mount(iface)
return pm_type, pm_dev return pm_type, pm_dev
end end
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
---@return string type, table device
function ppm.mount_virtual()
local iface = "ppm_vdev_" .. _ppm_sys.next_vid
_ppm_sys.mounts[iface] = peri_init("__virtual__")
_ppm_sys.next_vid = _ppm_sys.next_vid + 1
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
return _ppm_sys.mounts[iface].type, _ppm_sys.mounts[iface].dev
end
-- manually unmount a peripheral from the PPM -- manually unmount a peripheral from the PPM
---@param device table device table ---@param device table device table
function ppm.unmount(device) function ppm.unmount(device)

View File

@ -89,8 +89,12 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
-- parse the recorded advertisement and create unit sub-sessions -- parse the recorded advertisement and create unit sub-sessions
local function _handle_advertisement() local function _handle_advertisement()
self.units = {} _reset_config()
self.rs_io_q = {}
for i = 1, #self.f_units do
local unit = self.f_units[i] ---@type reactor_unit
unit.purge_rtu_devices(self.id)
end
for i = 1, #self.advert do for i = 1, #self.advert do
local unit = nil ---@type unit_session|nil local unit = nil ---@type unit_session|nil
@ -130,6 +134,7 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
if u_type == false then if u_type == false then
-- validation fail -- validation fail
log.debug(log_header .. "advertisement unit validation failure")
else else
local target_unit = self.f_units[unit_advert.reactor] ---@type reactor_unit local target_unit = self.f_units[unit_advert.reactor] ---@type reactor_unit
@ -285,8 +290,13 @@ function rtu.new_session(id, in_queue, out_queue, advertisement, facility_units)
_close() _close()
elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then
-- RTU unit advertisement -- RTU unit advertisement
-- handle advertisement; this will re-create all unit sub-sessions log.debug(log_header .. "received updated advertisement")
-- copy advertisement and remove version tag
self.advert = pkt.data self.advert = pkt.data
table.remove(self.advert, 1)
-- handle advertisement; this will re-create all unit sub-sessions
_handle_advertisement() _handle_advertisement()
elseif pkt.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT then elseif pkt.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT then
if pkt.length == 1 then if pkt.length == 1 then

View File

@ -46,7 +46,7 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): " local log_tag = "session.rtu(" .. session_id .. ").boilerv(" .. advert.index .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false, has_build = false,
periodics = { periodics = {
next_formed_req = 0, next_formed_req = 0,

View File

@ -37,7 +37,7 @@ function envd.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").envd(" .. advert.index .. "): " local log_tag = "session.rtu(" .. session_id .. ").envd(" .. advert.index .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
periodics = { periodics = {
next_rad_req = 0 next_rad_req = 0
}, },

View File

@ -46,7 +46,7 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").imatrix(" .. advert.index .. "): " local log_tag = "session.rtu(" .. session_id .. ").imatrix(" .. advert.index .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false, has_build = false,
periodics = { periodics = {
next_formed_req = 0, next_formed_req = 0,

View File

@ -61,7 +61,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. unit_id .. "): " local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. unit_id .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
in_q = mqueue.new(), in_q = mqueue.new(),
has_di = false, has_di = false,
has_ai = false, has_ai = false,

View File

@ -43,7 +43,7 @@ function sna.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").sna(" .. advert.index .. "): " local log_tag = "session.rtu(" .. session_id .. ").sna(" .. advert.index .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false, has_build = false,
periodics = { periodics = {
next_build_req = 0, next_build_req = 0,

View File

@ -46,7 +46,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").sps(" .. advert.index .. "): " local log_tag = "session.rtu(" .. session_id .. ").sps(" .. advert.index .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false, has_build = false,
periodics = { periodics = {
next_formed_req = 0, next_formed_req = 0,

View File

@ -66,7 +66,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
local log_tag = "session.rtu(" .. session_id .. ").turbinev(" .. advert.index .. "): " local log_tag = "session.rtu(" .. session_id .. ").turbinev(" .. advert.index .. "): "
local self = { local self = {
session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
in_q = mqueue.new(), in_q = mqueue.new(),
has_build = false, has_build = false,
periodics = { periodics = {

View File

@ -22,12 +22,13 @@ unit_session.RTU_US_CMDS = RTU_US_CMDS
unit_session.RTU_US_DATA = RTU_US_DATA unit_session.RTU_US_DATA = RTU_US_DATA
-- create a new unit session runner -- create a new unit session runner
---@param session_id integer RTU session ID
---@param unit_id integer MODBUS unit ID ---@param unit_id integer MODBUS unit ID
---@param advert rtu_advertisement RTU advertisement for this unit ---@param advert rtu_advertisement RTU advertisement for this unit
---@param out_queue mqueue send queue ---@param out_queue mqueue send queue
---@param log_tag string logging tag ---@param log_tag string logging tag
---@param txn_tags table transaction log tags ---@param txn_tags table transaction log tags
function unit_session.new(unit_id, advert, out_queue, log_tag, txn_tags) function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags)
local self = { local self = {
log_tag = log_tag, log_tag = log_tag,
txn_tags = txn_tags, txn_tags = txn_tags,
@ -132,6 +133,8 @@ function unit_session.new(unit_id, advert, out_queue, log_tag, txn_tags)
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- get the unit ID
function public.get_session_id() return session_id end
-- get the unit ID -- get the unit ID
function public.get_unit_id() return self.unit_id end function public.get_unit_id() return self.unit_id end
-- get the device index -- get the device index

View File

@ -401,6 +401,14 @@ function unit.new(for_reactor, num_boilers, num_turbines)
table.insert(self.redstone[field], accessor) table.insert(self.redstone[field], accessor)
end end
-- purge devices associated with the given RTU session ID
---@param session integer RTU session ID
function public.purge_rtu_devices(session)
util.filter_table(self.turbines, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.boilers, function (s) return s.get_session_id() ~= session end)
util.filter_table(self.redstone, function (s) return s.get_session_id() ~= session end)
end
-- UPDATE SESSION -- -- UPDATE SESSION --
-- update (iterate) this unit -- update (iterate) this unit

View File

@ -13,7 +13,7 @@ local svsessions = require("supervisor.session.svsessions")
local config = require("supervisor.config") local config = require("supervisor.config")
local supervisor = require("supervisor.supervisor") local supervisor = require("supervisor.supervisor")
local SUPERVISOR_VERSION = "beta-v0.7.3" local SUPERVISOR_VERSION = "beta-v0.7.4"
local print = util.print local print = util.print
local println = util.println local println = util.println