#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 renderer = require("coordinator.renderer")
local COORDINATOR_VERSION = "alpha-v0.6.9"
local COORDINATOR_VERSION = "alpha-v0.6.10"
local print = util.print
local println = util.println

View File

@ -13,7 +13,7 @@ local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc")
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 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 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
@ -257,98 +257,104 @@ local function configure()
local device = ppm.get_periph(name)
local type = nil
local rtu_iface = nil ---@type rtu_device
local rtu_type = ""
local formed = nil ---@type boolean|nil
if device == nil then
local message = util.c("configure> '", name, "' not found")
local message = util.c("configure> '", name, "' not found, using placeholder")
println(message)
log.fatal(message)
return false
log.warning(message)
-- mount a virtual (placeholder) device
type, device = ppm.mount_virtual()
else
local type = ppm.get_type(name)
local rtu_iface = nil ---@type rtu_device
local rtu_type = ""
local formed = nil ---@type boolean|nil
type = ppm.get_type(name)
end
if type == "boilerValve" then
-- boiler multiblock
rtu_type = rtu_t.boiler_valve
rtu_iface = boilerv_rtu.new(device)
formed = device.isFormed()
if type == "boilerValve" then
-- boiler multiblock
rtu_type = rtu_t.boiler_valve
rtu_iface = boilerv_rtu.new(device)
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock"))
return false
end
elseif type == "turbineValve" then
-- turbine multiblock
rtu_type = rtu_t.turbine_valve
rtu_iface = turbinev_rtu.new(device)
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
return false
end
elseif type == "inductionPort" then
-- induction matrix multiblock
rtu_type = rtu_t.induction_matrix
rtu_iface = imatrix_rtu.new(device)
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock"))
return false
end
elseif type == "spsPort" then
-- SPS multiblock
rtu_type = rtu_t.sps
rtu_iface = sps_rtu.new(device)
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock"))
return false
end
elseif type == "solarNeutronActivator" then
-- SNA
rtu_type = rtu_t.sna
rtu_iface = sna_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
rtu_type = rtu_t.env_detector
rtu_iface = envd_rtu.new(device)
else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
println_ts(message)
log.fatal(message)
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed boiler multiblock"))
return false
end
elseif type == "turbineValve" then
-- turbine multiblock
rtu_type = rtu_t.turbine_valve
rtu_iface = turbinev_rtu.new(device)
formed = device.isFormed()
if rtu_iface ~= nil then
---@class rtu_unit_registry_entry
local rtu_unit = {
name = name,
type = rtu_type,
index = index,
reactor = for_reactor,
device = device,
formed = formed,
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil
thread = nil
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, rtu_unit)
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor))
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed turbine multiblock"))
return false
end
elseif type == "inductionPort" then
-- induction matrix multiblock
rtu_type = rtu_t.induction_matrix
rtu_iface = imatrix_rtu.new(device)
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed induction matrix multiblock"))
return false
end
elseif type == "spsPort" then
-- SPS multiblock
rtu_type = rtu_t.sps
rtu_iface = sps_rtu.new(device)
formed = device.isFormed()
if formed == ppm.UNDEFINED_FIELD or formed == ppm.ACCESS_FAULT then
println_ts(util.c("configure> failed to check if '", name, "' is formed"))
log.fatal(util.c("configure> failed to check if '", name, "' is a formed SPS multiblock"))
return false
end
elseif type == "solarNeutronActivator" then
-- SNA
rtu_type = rtu_t.sna
rtu_iface = sna_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
rtu_type = rtu_t.env_detector
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
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
println_ts(message)
log.fatal(message)
return false
end
---@class rtu_unit_registry_entry
local rtu_unit = {
name = name,
type = rtu_type,
index = index,
reactor = for_reactor,
device = device,
formed = formed,
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil
thread = nil
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
table.insert(units, rtu_unit)
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for reactor ", for_reactor))
end
-- we made it through all that trusting-user-to-write-a-config-file chaos

View File

@ -122,9 +122,37 @@ function threads.thread__main(smem)
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then
local resend_advert = false
-- found, re-link
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
unit.rtu = boilerv_rtu.new(device)
unit.formed = true
@ -142,14 +170,19 @@ function threads.thread__main(smem)
elseif unit.type == rtu_t.env_detector then
unit.rtu = envd_rtu.new(device)
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
unit.modbus_io = modbus.new(unit.rtu, true)
rtu_comms.send_remounted(unit.index)
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
@ -274,7 +307,6 @@ function threads.thread__unit_comms(smem, unit)
local last_update = util.time()
local check_formed = type(unit.formed) == "boolean"
local last_f_check = 0
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
-- check if multiblocks is still formed
if check_formed and (util.time() - last_f_check > 1000) then
-- check if multiblock is still formed if this is a multiblock
if (type(unit.formed) == "boolean") and (util.time() - last_f_check > 1000) then
if (not unit.formed) and unit.device.isFormed() then
-- newly re-formed
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
elseif type == rtu_t.induction_matrix then
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
return nil
@ -659,6 +665,12 @@ function comms.advert_type_to_rtu_t(utype)
return rtu_t.turbine_valve
elseif utype == RTU_UNIT_TYPES.IMATRIX then
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
return nil

View File

@ -58,6 +58,7 @@ end
-- private log write function
---@param msg string
local function _log(msg)
local out_of_space = false
local time_stamp = os.date("[%c] ")
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 not status then
if result == "Out of space" then
if (not status) and (result ~= nil) then
out_of_space = string.find(result, "Out of space") ~= nil
if out_of_space then
-- will delete log file
elseif result ~= nil then
else
util.println("unknown error writing to logfile: " .. result)
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
_log_sys.file.close()
fs.delete(_log_sys.path)

View File

@ -12,8 +12,11 @@ local ACCESS_FAULT = nil ---@type nil
local UNDEFINED_FIELD = "undefined field"
local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
ppm.ACCESS_FAULT = ACCESS_FAULT
ppm.UNDEFINED_FIELD = UNDEFINED_FIELD
ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
----------------------------
-- PRIVATE DATA/FUNCTIONS --
@ -23,6 +26,7 @@ local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local _ppm_sys = {
mounts = {},
next_vid = 0,
auto_cf = false,
faulted = false,
last_fault = "",
@ -42,10 +46,15 @@ local function peri_init(iface)
last_fault = "",
fault_counts = {},
auto_cf = true,
type = peripheral.getType(iface),
device = peripheral.wrap(iface)
type = VIRTUAL_DEVICE_TYPE,
device = {}
}
if iface ~= "__virtual__" then
self.type = peripheral.getType(iface)
self.device = peripheral.wrap(iface)
end
-- initialization process (re-map)
for key, func in pairs(self.device) do
@ -245,6 +254,19 @@ function ppm.mount(iface)
return pm_type, pm_dev
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
---@param device table device table
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
local function _handle_advertisement()
self.units = {}
self.rs_io_q = {}
_reset_config()
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
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
-- validation fail
log.debug(log_header .. "advertisement unit validation failure")
else
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()
elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then
-- 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
table.remove(self.advert, 1)
-- handle advertisement; this will re-create all unit sub-sessions
_handle_advertisement()
elseif pkt.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT 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 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,
periodics = {
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 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 = {
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 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,
periodics = {
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 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(),
has_di = 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 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,
periodics = {
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 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,
periodics = {
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 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(),
has_build = false,
periodics = {

View File

@ -22,12 +22,13 @@ unit_session.RTU_US_CMDS = RTU_US_CMDS
unit_session.RTU_US_DATA = RTU_US_DATA
-- create a new unit session runner
---@param session_id integer RTU session ID
---@param unit_id integer MODBUS unit ID
---@param advert rtu_advertisement RTU advertisement for this unit
---@param out_queue mqueue send queue
---@param log_tag string logging tag
---@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 = {
log_tag = log_tag,
txn_tags = txn_tags,
@ -132,6 +133,8 @@ function unit_session.new(unit_id, advert, out_queue, log_tag, txn_tags)
-- PUBLIC FUNCTIONS --
-- get the unit ID
function public.get_session_id() return session_id end
-- get the unit ID
function public.get_unit_id() return self.unit_id end
-- 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)
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 (iterate) this unit

View File

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