From 88862726e32fc349a97cff626f3dc49e0f777a4e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 18 Oct 2025 12:37:27 -0400 Subject: [PATCH] migrate RTU initialization to new file --- rtu/startup.lua | 444 +----------------------------------------------- rtu/uinit.lua | 441 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 448 insertions(+), 437 deletions(-) create mode 100644 rtu/uinit.lua diff --git a/rtu/startup.lua b/rtu/startup.lua index 920b0d9..089053d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -1,5 +1,5 @@ -- --- RTU: Remote Terminal Unit +-- RTU Gateway: Remote Terminal Unit Gateway -- require("/initenv").init_env() @@ -11,31 +11,17 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local network = require("scada-common.network") local ppm = require("scada-common.ppm") -local rsio = require("scada-common.rsio") -local types = require("scada-common.types") local util = require("scada-common.util") local configure = require("rtu.configure") local databus = require("rtu.databus") -local modbus = require("rtu.modbus") local renderer = require("rtu.renderer") local rtu = require("rtu.rtu") local threads = require("rtu.threads") - -local boilerv_rtu = require("rtu.dev.boilerv_rtu") -local dynamicv_rtu = require("rtu.dev.dynamicv_rtu") -local envd_rtu = require("rtu.dev.envd_rtu") -local imatrix_rtu = require("rtu.dev.imatrix_rtu") -local redstone_rtu = require("rtu.dev.redstone_rtu") -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 uinit = require("rtu.uinit") local RTU_VERSION = "v1.12.3" -local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE -local RTU_HW_STATE = databus.RTU_HW_STATE - local println = util.println local println_ts = util.println_ts @@ -128,439 +114,23 @@ local function main() } } - local smem_sys = __shared_memory.rtu_sys - local smem_dev = __shared_memory.rtu_dev - + local smem_sys = __shared_memory.rtu_sys + local smem_dev = __shared_memory.rtu_dev local rtu_state = __shared_memory.rtu_state + local units = __shared_memory.rtu_sys.units -- get the configured modem if smem_dev.modem_wired then smem_dev.modem = ppm.get_wired_modem(smem_dev.modem_iface) else smem_dev.modem = ppm.get_wireless_modem() end - ---------------------------------------- - -- interpret RTU configs and init units - ---------------------------------------- - - local units = __shared_memory.rtu_sys.units - - local rtu_redstone = config.Redstone - local rtu_devices = config.Peripherals - - -- print and log a fatal error during startup - ---@param msg string - local function log_fail(msg) - println(msg) - log.fatal(msg) - end - - -- get a string representation of a port interface - ---@param entry rtu_rs_definition - ---@return string - local function entry_iface_name(entry) - return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) - end - - -- configure RTU gateway based on settings file definitions - local function sys_config() - --#region Redstone Interfaces - - local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[] - local all_conns = { [0] = {}, {}, {}, {}, {} } - - -- go through redstone definitions list - for entry_idx = 1, #rtu_redstone do - local entry = rtu_redstone[entry_idx] - - local assignment - local for_reactor = entry.unit - local phy = entry.relay or 0 - local phy_name = entry.relay or "local" - local iface_name = entry_iface_name(entry) - - if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then - ---@cast for_reactor integer - assignment = "reactor unit " .. entry.unit - elseif entry.unit == nil then - assignment = "facility" - for_reactor = 0 - else - log_fail(util.c("sys_config> invalid unit assignment at block index #", entry_idx)) - return false - end - - -- create the appropriate RTU if it doesn't exist and check relay name validity - if entry.relay then - if type(entry.relay) ~= "string" then - log_fail(util.c("sys_config> invalid redstone relay '", entry.relay, '"')) - return false - elseif not rs_rtus[entry.relay] then - log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay)) - - local hw_state = RTU_HW_STATE.OK - local relay = ppm.get_periph(entry.relay) - - if not relay then - hw_state = RTU_HW_STATE.OFFLINE - log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected")) - local _, v_device = ppm.mount_virtual() - relay = v_device - elseif ppm.get_type(entry.relay) ~= "redstone_relay" then - hw_state = RTU_HW_STATE.FAULTED - log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay")) - end - - rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } } - end - elseif rs_rtus[0] == nil then - log.debug(util.c("sys_config> allocated local redstone RTU")) - rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } } - end - - -- verify configuration - local valid = false - if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then - valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color)) - end - - local bank = rs_rtus[phy].banks[for_reactor] - local conns = all_conns[for_reactor] - - if not valid then - log_fail(util.c("sys_config> invalid redstone definition at block index #", entry_idx)) - return false - else - -- link redstone in RTU - local mode = rsio.get_io_mode(entry.port) - if mode == rsio.IO_MODE.DIGITAL_IN then - -- can't have duplicate inputs - if util.table_contains(conns, entry.port) then - local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name) - println(message) - log.warning(message) - else - table.insert(bank, entry) - end - elseif mode == rsio.IO_MODE.ANALOG_IN then - -- can't have duplicate inputs - if util.table_contains(conns, entry.port) then - local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name) - println(message) - log.warning(message) - else - table.insert(bank, entry) - end - elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then - table.insert(bank, entry) - else - -- should be unreachable code, we already validated ports - log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx) - println("sys_config> encountered a software error, check logs") - return false - end - - table.insert(conns, entry.port) - - log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment)) - end - end - - -- create unit entries for redstone RTUs - for _, def in pairs(rs_rtus) do - local rtu_conns = { [0] = {}, {}, {}, {}, {} } - - -- connect the IO banks - for for_reactor = 0, #def.banks do - local bank = def.banks[for_reactor] - local conns = rtu_conns[for_reactor] - local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility") - - -- link redstone to the RTU - for i = 1, #bank do - local conn = bank[i] - local phy_name = conn.relay or "local" - - local mode = rsio.get_io_mode(conn.port) - if mode == rsio.IO_MODE.DIGITAL_IN then - def.rtu.link_di(conn.side, conn.color, conn.invert) - elseif mode == rsio.IO_MODE.DIGITAL_OUT then - def.rtu.link_do(conn.side, conn.color, conn.invert) - elseif mode == rsio.IO_MODE.ANALOG_IN then - def.rtu.link_ai(conn.side) - elseif mode == rsio.IO_MODE.ANALOG_OUT then - def.rtu.link_ao(conn.side) - else - log.fatal(util.c("sys_config> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign)) - println("sys_config> encountered a software error, check logs") - return false - end - - table.insert(conns, conn.port) - - log.debug(util.c("sys_config> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign)) - end - end - - ---@type rtu_registry_entry - local unit = { - uid = 0, - name = def.name, - type = RTU_UNIT_TYPE.REDSTONE, - index = false, - reactor = nil, - device = def.phy, - rs_conns = rtu_conns, - is_multiblock = false, - formed = nil, - hw_state = def.hw_state, - rtu = def.rtu, - modbus_io = modbus.new(def.rtu, false), - pkt_queue = nil, - thread = nil - } - - table.insert(units, unit) - - local type = util.trinary(def.phy == rs, "redstone", "redstone_relay") - - log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")")) - - unit.uid = #units - - databus.tx_unit_hw_status(unit.uid, unit.hw_state) - end - - --#endregion - --#region Mounted Peripherals - - for i = 1, #rtu_devices do - local entry = rtu_devices[i] ---@type rtu_peri_definition - local name = entry.name - local index = entry.index - local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit) - - -- CHECK: name is a string - if type(name) ~= "string" then - log_fail(util.c("sys_config> device entry #", i, ": device ", name, " isn't a string")) - return false - end - - -- CHECK: index type - if (index ~= nil) and (not util.is_int(index)) then - log_fail(util.c("sys_config> device entry #", i, ": index ", index, " isn't valid")) - return false - end - - -- CHECK: index range - local function validate_index(min, max) - if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then - local message = util.c("sys_config> device entry #", i, ": index ", index, " isn't >= ", min) - if max ~= nil then message = util.c(message, " and <= ", max) end - log_fail(message) - return false - else return true end - end - - -- CHECK: reactor is an integer >= 0 - local function validate_assign(for_facility) - if for_facility and for_reactor ~= 0 then - log_fail(util.c("sys_config> device entry #", i, ": must only be for the facility")) - return false - elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then - log_fail(util.c("sys_config> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")) - return false - else return true end - end - - local device = ppm.get_periph(name) - - local type ---@type string|nil - local rtu_iface ---@type rtu_device - local rtu_type ---@type RTU_UNIT_TYPE - local is_multiblock = false ---@type boolean - local formed = nil ---@type boolean|nil - local faulted = nil ---@type boolean|nil - - if device == nil then - local message = util.c("sys_config> '", 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 - -- boiler multiblock - if not validate_index(1, 2) then return false end - if not validate_assign() then return false end - - rtu_type = RTU_UNIT_TYPE.BOILER_VALVE - rtu_iface, faulted = boilerv_rtu.new(device) - is_multiblock = true - formed = device.isFormed() - - if formed == ppm.ACCESS_FAULT then - println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) - log.warning(util.c("sys_config> failed to check if '", name, "' is a formed boiler multiblock")) - end - elseif type == "turbineValve" then - -- turbine multiblock - if not validate_index(1, 3) then return false end - if not validate_assign() then return false end - - rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE - rtu_iface, faulted = turbinev_rtu.new(device) - is_multiblock = true - formed = device.isFormed() - - if formed == ppm.ACCESS_FAULT then - println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) - log.warning(util.c("sys_config> failed to check if '", name, "' is a formed turbine multiblock")) - end - elseif type == "dynamicValve" then - -- dynamic tank multiblock - if entry.unit == nil then - if not validate_index(1, 4) then return false end - if not validate_assign(true) then return false end - else - if not validate_index(1, 1) then return false end - if not validate_assign() then return false end - end - - rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE - rtu_iface, faulted = dynamicv_rtu.new(device) - is_multiblock = true - formed = device.isFormed() - - if formed == ppm.ACCESS_FAULT then - println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) - log.warning(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock")) - end - elseif type == "inductionPort" or type == "reinforcedInductionPort" then - -- induction matrix multiblock (normal or reinforced) - if not validate_assign(true) then return false end - - rtu_type = RTU_UNIT_TYPE.IMATRIX - rtu_iface, faulted = imatrix_rtu.new(device) - is_multiblock = true - formed = device.isFormed() - - if formed == ppm.ACCESS_FAULT then - println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) - log.warning(util.c("sys_config> failed to check if '", name, "' is a formed induction matrix multiblock")) - end - elseif type == "spsPort" then - -- SPS multiblock - if not validate_assign(true) then return false end - - rtu_type = RTU_UNIT_TYPE.SPS - rtu_iface, faulted = sps_rtu.new(device) - is_multiblock = true - formed = device.isFormed() - - if formed == ppm.ACCESS_FAULT then - println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) - log.warning(util.c("sys_config> failed to check if '", name, "' is a formed SPS multiblock")) - end - elseif type == "solarNeutronActivator" then - -- SNA - if not validate_assign() then return false end - - rtu_type = RTU_UNIT_TYPE.SNA - rtu_iface, faulted = sna_rtu.new(device) - elseif type == "environmentDetector" or type == "environment_detector" then - -- advanced peripherals environment detector - if not validate_index(1) then return false end - if not validate_assign(entry.unit == nil) then return false end - - rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR - rtu_iface, faulted = envd_rtu.new(device) - elseif type == ppm.VIRTUAL_DEVICE_TYPE then - -- placeholder device - rtu_type = RTU_UNIT_TYPE.VIRTUAL - rtu_iface = rtu.init_unit().interface() - else - log_fail(util.c("sys_config> device '", name, "' is not a known type (", type, ")")) - return false - end - - if is_multiblock then - if not formed then - if formed == false then - log.info(util.c("sys_config> device '", name, "' is not formed")) - else formed = false end - elseif faulted then - -- sometimes there is a race condition on server boot where it reports formed, but - -- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later - formed = false - log.warning(util.c("sys_config> device '", name, "' is formed, but initialization had one or more faults: marked as unformed")) - end - end - - ---@class rtu_registry_entry - local rtu_unit = { - uid = 0, ---@type integer RTU unit ID - name = name, ---@type string unit name - type = rtu_type, ---@type RTU_UNIT_TYPE unit type - index = index or false, ---@type integer|false device index - reactor = for_reactor, ---@type integer|nil unit/facility assignment - device = device, ---@type table peripheral reference - rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections - is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral - formed = formed, ---@type boolean|nil if this peripheral is currently formed - hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status - rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface - modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface - pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue - thread = nil ---@type parallel_thread|nil associated RTU thread - } - - rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) - - table.insert(units, rtu_unit) - - local for_message = "the facility" - if for_reactor > 0 then - for_message = util.c("reactor ", for_reactor) - end - - local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "") - log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message)) - - rtu_unit.uid = #units - - -- determine hardware status - if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then - rtu_unit.hw_state = RTU_HW_STATE.OFFLINE - else - if rtu_unit.is_multiblock then - rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED) - elseif faulted then - rtu_unit.hw_state = RTU_HW_STATE.FAULTED - else - rtu_unit.hw_state = RTU_HW_STATE.OK - end - end - - -- report hardware status - databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) - end - - --#endregion - - return true - end - ---------------------------------------- -- start system ---------------------------------------- - log.debug("boot> running sys_config()") + log.debug("boot> running uinit()") - if sys_config() then + if uinit(config, __shared_memory) then -- check comms modem if smem_dev.modem == nil then println("startup> comms modem not found") diff --git a/rtu/uinit.lua b/rtu/uinit.lua new file mode 100644 index 0000000..8892c2e --- /dev/null +++ b/rtu/uinit.lua @@ -0,0 +1,441 @@ +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local databus = require("rtu.databus") +local modbus = require("rtu.modbus") +local rtu = require("rtu.rtu") +local threads = require("rtu.threads") + +local boilerv_rtu = require("rtu.dev.boilerv_rtu") +local dynamicv_rtu = require("rtu.dev.dynamicv_rtu") +local envd_rtu = require("rtu.dev.envd_rtu") +local imatrix_rtu = require("rtu.dev.imatrix_rtu") +local redstone_rtu = require("rtu.dev.redstone_rtu") +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 println = util.println +local println_ts = util.println_ts + +local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE +local RTU_HW_STATE = databus.RTU_HW_STATE + +-- print and log a fatal error during startup +---@param msg string +local function log_fail(msg) + println(msg) + log.fatal(msg) +end + +-- get a string representation of a port interface +---@param entry rtu_rs_definition +---@return string +local function entry_iface_name(entry) + return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) +end + +-- configure RTU gateway based on settings file definitions +---@param config rtu_config +---@param __shared_memory rtu_shared_memory +---@return boolean success +return function(config, __shared_memory) + local units = __shared_memory.rtu_sys.units + + local rtu_redstone = config.Redstone + local rtu_devices = config.Peripherals + + --#region Redstone Interfaces + + local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[] + local all_conns = { [0] = {}, {}, {}, {}, {} } + + -- go through redstone definitions list + for entry_idx = 1, #rtu_redstone do + local entry = rtu_redstone[entry_idx] + + local assignment + local for_reactor = entry.unit + local phy = entry.relay or 0 + local phy_name = entry.relay or "local" + local iface_name = entry_iface_name(entry) + + if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then + ---@cast for_reactor integer + assignment = "reactor unit " .. entry.unit + elseif entry.unit == nil then + assignment = "facility" + for_reactor = 0 + else + log_fail(util.c("uinit> invalid unit assignment at block index #", entry_idx)) + return false + end + + -- create the appropriate RTU if it doesn't exist and check relay name validity + if entry.relay then + if type(entry.relay) ~= "string" then + log_fail(util.c("uinit> invalid redstone relay '", entry.relay, '"')) + return false + elseif not rs_rtus[entry.relay] then + log.debug(util.c("uinit> allocated relay redstone RTU on interface ", entry.relay)) + + local hw_state = RTU_HW_STATE.OK + local relay = ppm.get_periph(entry.relay) + + if not relay then + hw_state = RTU_HW_STATE.OFFLINE + log.warning(util.c("uinit> redstone relay ", entry.relay, " is not connected")) + local _, v_device = ppm.mount_virtual() + relay = v_device + elseif ppm.get_type(entry.relay) ~= "redstone_relay" then + hw_state = RTU_HW_STATE.FAULTED + log.warning(util.c("uinit> redstone relay ", entry.relay, " is not a redstone relay")) + end + + rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } } + end + elseif rs_rtus[0] == nil then + log.debug(util.c("uinit> allocated local redstone RTU")) + rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } } + end + + -- verify configuration + local valid = false + if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then + valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color)) + end + + local bank = rs_rtus[phy].banks[for_reactor] + local conns = all_conns[for_reactor] + + if not valid then + log_fail(util.c("uinit> invalid redstone definition at block index #", entry_idx)) + return false + else + -- link redstone in RTU + local mode = rsio.get_io_mode(entry.port) + if mode == rsio.IO_MODE.DIGITAL_IN then + -- can't have duplicate inputs + if util.table_contains(conns, entry.port) then + local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name) + println(message) + log.warning(message) + else + table.insert(bank, entry) + end + elseif mode == rsio.IO_MODE.ANALOG_IN then + -- can't have duplicate inputs + if util.table_contains(conns, entry.port) then + local message = util.c("uinit> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name) + println(message) + log.warning(message) + else + table.insert(bank, entry) + end + elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then + table.insert(bank, entry) + else + -- should be unreachable code, we already validated ports + log.fatal("uinit> failed to identify IO mode at block index #" .. entry_idx) + println("uinit> encountered a software error, check logs") + return false + end + + table.insert(conns, entry.port) + + log.debug(util.c("uinit> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment)) + end + end + + -- create unit entries for redstone RTUs + for _, def in pairs(rs_rtus) do + local rtu_conns = { [0] = {}, {}, {}, {}, {} } + + -- connect the IO banks + for for_reactor = 0, #def.banks do + local bank = def.banks[for_reactor] + local conns = rtu_conns[for_reactor] + local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility") + + -- link redstone to the RTU + for i = 1, #bank do + local conn = bank[i] + local phy_name = conn.relay or "local" + + local mode = rsio.get_io_mode(conn.port) + if mode == rsio.IO_MODE.DIGITAL_IN then + def.rtu.link_di(conn.side, conn.color, conn.invert) + elseif mode == rsio.IO_MODE.DIGITAL_OUT then + def.rtu.link_do(conn.side, conn.color, conn.invert) + elseif mode == rsio.IO_MODE.ANALOG_IN then + def.rtu.link_ai(conn.side) + elseif mode == rsio.IO_MODE.ANALOG_OUT then + def.rtu.link_ao(conn.side) + else + log.fatal(util.c("uinit> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign)) + println("uinit> encountered a software error, check logs") + return false + end + + table.insert(conns, conn.port) + + log.debug(util.c("uinit> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign)) + end + end + + ---@type rtu_registry_entry + local unit = { + uid = 0, + name = def.name, + type = RTU_UNIT_TYPE.REDSTONE, + index = false, + reactor = nil, + device = def.phy, + rs_conns = rtu_conns, + is_multiblock = false, + formed = nil, + hw_state = def.hw_state, + rtu = def.rtu, + modbus_io = modbus.new(def.rtu, false), + pkt_queue = nil, + thread = nil + } + + table.insert(units, unit) + + local type = util.trinary(def.phy == rs, "redstone", "redstone_relay") + + log.info(util.c("uinit> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")")) + + unit.uid = #units + + databus.tx_unit_hw_status(unit.uid, unit.hw_state) + end + + --#endregion + --#region Mounted Peripherals + + for i = 1, #rtu_devices do + local entry = rtu_devices[i] ---@type rtu_peri_definition + local name = entry.name + local index = entry.index + local for_reactor = util.trinary(entry.unit == nil, 0, entry.unit) + + -- CHECK: name is a string + if type(name) ~= "string" then + log_fail(util.c("uinit> device entry #", i, ": device ", name, " isn't a string")) + return false + end + + -- CHECK: index type + if (index ~= nil) and (not util.is_int(index)) then + log_fail(util.c("uinit> device entry #", i, ": index ", index, " isn't valid")) + return false + end + + -- CHECK: index range + local function validate_index(min, max) + if (not util.is_int(index)) or ((index < min) and (max ~= nil and index > max)) then + local message = util.c("uinit> device entry #", i, ": index ", index, " isn't >= ", min) + if max ~= nil then message = util.c(message, " and <= ", max) end + log_fail(message) + return false + else return true end + end + + -- CHECK: reactor is an integer >= 0 + local function validate_assign(for_facility) + if for_facility and for_reactor ~= 0 then + log_fail(util.c("uinit> device entry #", i, ": must only be for the facility")) + return false + elseif (not for_facility) and ((not util.is_int(for_reactor)) or (for_reactor < 1) or (for_reactor > 4)) then + log_fail(util.c("uinit> device entry #", i, ": unit assignment ", for_reactor, " isn't vaild")) + return false + else return true end + end + + local device = ppm.get_periph(name) + + local type ---@type string|nil + local rtu_iface ---@type rtu_device + local rtu_type ---@type RTU_UNIT_TYPE + local is_multiblock = false ---@type boolean + local formed = nil ---@type boolean|nil + local faulted = nil ---@type boolean|nil + + if device == nil then + local message = util.c("uinit> '", 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 + -- boiler multiblock + if not validate_index(1, 2) then return false end + if not validate_assign() then return false end + + rtu_type = RTU_UNIT_TYPE.BOILER_VALVE + rtu_iface, faulted = boilerv_rtu.new(device) + is_multiblock = true + formed = device.isFormed() + + if formed == ppm.ACCESS_FAULT then + println_ts(util.c("uinit> failed to check if '", name, "' is formed")) + log.warning(util.c("uinit> failed to check if '", name, "' is a formed boiler multiblock")) + end + elseif type == "turbineValve" then + -- turbine multiblock + if not validate_index(1, 3) then return false end + if not validate_assign() then return false end + + rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE + rtu_iface, faulted = turbinev_rtu.new(device) + is_multiblock = true + formed = device.isFormed() + + if formed == ppm.ACCESS_FAULT then + println_ts(util.c("uinit> failed to check if '", name, "' is formed")) + log.warning(util.c("uinit> failed to check if '", name, "' is a formed turbine multiblock")) + end + elseif type == "dynamicValve" then + -- dynamic tank multiblock + if entry.unit == nil then + if not validate_index(1, 4) then return false end + if not validate_assign(true) then return false end + else + if not validate_index(1, 1) then return false end + if not validate_assign() then return false end + end + + rtu_type = RTU_UNIT_TYPE.DYNAMIC_VALVE + rtu_iface, faulted = dynamicv_rtu.new(device) + is_multiblock = true + formed = device.isFormed() + + if formed == ppm.ACCESS_FAULT then + println_ts(util.c("uinit> failed to check if '", name, "' is formed")) + log.warning(util.c("uinit> failed to check if '", name, "' is a formed dynamic tank multiblock")) + end + elseif type == "inductionPort" or type == "reinforcedInductionPort" then + -- induction matrix multiblock (normal or reinforced) + if not validate_assign(true) then return false end + + rtu_type = RTU_UNIT_TYPE.IMATRIX + rtu_iface, faulted = imatrix_rtu.new(device) + is_multiblock = true + formed = device.isFormed() + + if formed == ppm.ACCESS_FAULT then + println_ts(util.c("uinit> failed to check if '", name, "' is formed")) + log.warning(util.c("uinit> failed to check if '", name, "' is a formed induction matrix multiblock")) + end + elseif type == "spsPort" then + -- SPS multiblock + if not validate_assign(true) then return false end + + rtu_type = RTU_UNIT_TYPE.SPS + rtu_iface, faulted = sps_rtu.new(device) + is_multiblock = true + formed = device.isFormed() + + if formed == ppm.ACCESS_FAULT then + println_ts(util.c("uinit> failed to check if '", name, "' is formed")) + log.warning(util.c("uinit> failed to check if '", name, "' is a formed SPS multiblock")) + end + elseif type == "solarNeutronActivator" then + -- SNA + if not validate_assign() then return false end + + rtu_type = RTU_UNIT_TYPE.SNA + rtu_iface, faulted = sna_rtu.new(device) + elseif type == "environmentDetector" or type == "environment_detector" then + -- advanced peripherals environment detector + if not validate_index(1) then return false end + if not validate_assign(entry.unit == nil) then return false end + + rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR + rtu_iface, faulted = envd_rtu.new(device) + elseif type == ppm.VIRTUAL_DEVICE_TYPE then + -- placeholder device + rtu_type = RTU_UNIT_TYPE.VIRTUAL + rtu_iface = rtu.init_unit().interface() + else + log_fail(util.c("uinit> device '", name, "' is not a known type (", type, ")")) + return false + end + + if is_multiblock then + if not formed then + if formed == false then + log.info(util.c("uinit> device '", name, "' is not formed")) + else formed = false end + elseif faulted then + -- sometimes there is a race condition on server boot where it reports formed, but + -- the other functions are not yet defined (that's the theory at least). mark as unformed to attempt connection later + formed = false + log.warning(util.c("uinit> device '", name, "' is formed, but initialization had one or more faults: marked as unformed")) + end + end + + ---@class rtu_registry_entry + local rtu_unit = { + uid = 0, ---@type integer RTU unit ID + name = name, ---@type string unit name + type = rtu_type, ---@type RTU_UNIT_TYPE unit type + index = index or false, ---@type integer|false device index + reactor = for_reactor, ---@type integer|nil unit/facility assignment + device = device, ---@type table peripheral reference + rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections + is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral + formed = formed, ---@type boolean|nil if this peripheral is currently formed + hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status + rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface + modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface + pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue + thread = nil ---@type parallel_thread|nil associated RTU thread + } + + rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) + + table.insert(units, rtu_unit) + + local for_message = "the facility" + if for_reactor > 0 then + for_message = util.c("reactor ", for_reactor) + end + + local index_str = util.trinary(index ~= nil, util.c(" [", index, "]"), "") + log.info(util.c("uinit> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ")", index_str, " for ", for_message)) + + rtu_unit.uid = #units + + -- determine hardware status + if rtu_unit.type == RTU_UNIT_TYPE.VIRTUAL then + rtu_unit.hw_state = RTU_HW_STATE.OFFLINE + else + if rtu_unit.is_multiblock then + rtu_unit.hw_state = util.trinary(rtu_unit.formed == true, RTU_HW_STATE.OK, RTU_HW_STATE.UNFORMED) + elseif faulted then + rtu_unit.hw_state = RTU_HW_STATE.FAULTED + else + rtu_unit.hw_state = RTU_HW_STATE.OK + end + end + + -- report hardware status + databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) + end + + --#endregion + + return true +end