add support for group affiliation timeout similar to unit registration timeout (should handle #122); add support for bulk announcement of peer unit registration data to the FNE; move announcement handling from main traffic port to metadata port (for now there is a code redirect on the traffic port that will be maintained for a few versions before being deprecated;

pull/121/merge
Bryan Biedenkapp 2 weeks ago
parent 3d6a196ea8
commit 06df86e713

@ -185,6 +185,8 @@ protocols:
dumpCsbkData: false
# Flag indicating unit registration will be verified after some operations.
verifyReg: false
# Flag indicating automated 12-hour idle group affiliation timeout is disabled.
disableGrpAffTimeout: false
# Flag indicating automated 12-hour idle unit registration timeout is disabled.
disableUnitRegTimeout: false
# Specifies the random wait delay for a subscriber.
@ -286,6 +288,8 @@ protocols:
verifyAff: false
# Flag indicating the host should verify unit registration.
verifyReg: false
# Flag indicating automated 12-hour idle group affiliation timeout is disabled.
disableGrpAffTimeout: false
# Flag indicating automated 12-hour idle unit registration timeout is disabled.
disableUnitRegTimeout: false
# Flag indicating the host requires LLA verification before allowing unit registration.
@ -361,6 +365,8 @@ protocols:
verifyAff: false
# Flag indicating the host should verify unit registration.
verifyReg: false
# Flag indicating automated 12-hour idle group affiliation timeout is disabled.
disableGrpAffTimeout: false
# Flag indicating automated 12-hour idle unit registration timeout is disabled.
disableUnitRegTimeout: false
# Flag indicating whether verbose dumping of NXDN RCCH data is enabled.

@ -19,6 +19,7 @@ using namespace lookups;
// ---------------------------------------------------------------------------
const uint32_t UNIT_REG_TIMEOUT = 43200U; // 12 hours
const uint32_t GRP_AFF_TIMEOUT = 43200U; // 12 hours
// ---------------------------------------------------------------------------
// Public Class Members
@ -31,15 +32,18 @@ AffiliationLookup::AffiliationLookup(const std::string name, ChannelLookup* chan
m_unitRegTable(),
m_unitRegTimers(),
m_grpAffTable(),
m_grpAffTimers(),
m_grantChTable(),
m_grantSrcIdTable(),
m_uuGrantedTable(),
m_netGrantedTable(),
m_grantTimers(),
m_releaseGrant(nullptr),
m_unitDereg(nullptr),
m_name(),
m_chLookup(channelLookup),
m_disableUnitRegTimeout(false),
m_disableGrpAffTimeout(false),
m_verbose(verbose)
{
m_name = name;
@ -47,6 +51,7 @@ AffiliationLookup::AffiliationLookup(const std::string name, ChannelLookup* chan
m_unitRegTable.clear();
m_unitRegTimers.clear();
m_grpAffTable.clear();
m_grpAffTimers.clear();
m_grantChTable.clear();
m_grantSrcIdTable.clear();
@ -133,9 +138,15 @@ void AffiliationLookup::touchUnitReg(uint32_t srcId)
__spinlock();
// restart the unit registration timer
if (isUnitReg(srcId)) {
m_unitRegTimers[srcId].start();
}
// restart the group affiliation timer if the source ID is group affiliated
if (isSrcIdGrpAff(srcId)) {
m_grpAffTimers[srcId].start();
}
}
/* Gets the current timer timeout for this unit registration. */
@ -195,7 +206,6 @@ bool AffiliationLookup::isUnitReg(uint32_t srcId) const
void AffiliationLookup::clearUnitReg()
{
__lock();
std::vector<uint32_t> srcToRel = std::vector<uint32_t>();
LogWarning(LOG_HOST, "%s, releasing all unit registrations", m_name.c_str());
m_unitRegTable.clear();
__unlock();
@ -211,6 +221,9 @@ void AffiliationLookup::groupAff(uint32_t srcId, uint32_t dstId)
// update dynamic affiliation table
m_grpAffTable[srcId] = dstId;
m_grpAffTimers[srcId] = Timer(1000U, GRP_AFF_TIMEOUT);
m_grpAffTimers[srcId].start();
if (m_verbose) {
LogInfoEx(LOG_HOST, "%s, group affiliation, srcId = %u, dstId = %u",
m_name.c_str(), srcId, dstId);
@ -233,23 +246,17 @@ bool AffiliationLookup::groupUnaff(uint32_t srcId)
LogInfoEx(LOG_HOST, "%s, group unaffiliation, srcId = %u, dstId = %u",
m_name.c_str(), srcId, it->second);
}
} else {
__unlock();
return false;
}
// remove dynamic affiliation table entry
try {
uint32_t entry = m_grpAffTable.at(srcId); // this value will get discarded
(void)entry; // but some variants of C++ mark the unordered_map<>::at as nodiscard
m_grpAffTable.erase(srcId);
m_grpAffTimers[srcId].stop();
__unlock();
return true;
}
catch (...) {
__unlock();
return false;
}
}
/* Helper to determine if the group destination ID has any affiations. */
@ -689,6 +696,31 @@ void AffiliationLookup::clock(uint32_t ms)
releaseGrant(dstId, false);
}
if (!m_disableGrpAffTimeout) {
m_grpAffTable.spinlock();
// clock all the group affiliation timers
m_grpAffTable.lock(false);
std::vector<uint32_t> affsToRel = std::vector<uint32_t>();
for (auto entry : m_grpAffTable) {
uint32_t srcId = entry.first;
auto it = m_grpAffTimers.find(srcId);
if (it != m_grpAffTimers.end()) {
it->second.clock(ms);
if (it->second.isRunning() && it->second.hasExpired()) {
affsToRel.push_back(srcId);
}
}
}
m_grpAffTable.unlock();
// release group affiliations that have timed out
for (uint32_t srcId : affsToRel) {
LogWarning(LOG_HOST, "%s, clearing stale group affiliation, srcId = %u", m_name.c_str(), srcId);
groupUnaff(srcId);
}
}
if (!m_disableUnitRegTimeout) {
m_unitRegTable.spinlock();
@ -708,7 +740,30 @@ void AffiliationLookup::clock(uint32_t ms)
// release units registrations that have timed out
for (uint32_t srcId : unitsToDereg) {
LogWarning(LOG_HOST, "%s, clearing stale unit deregistration, srcId = %u", m_name.c_str(), srcId);
unitDereg(srcId, true);
}
}
}
// ---------------------------------------------------------------------------
// Protected Class Members
// ---------------------------------------------------------------------------
/* Helper to determine if the source ID has group affiliations. */
bool AffiliationLookup::isSrcIdGrpAff(uint32_t srcId) const
{
__spinlock();
// lookup dynamic affiliation table entry
m_grpAffTable.lock(false);
auto it = m_grpAffTable.find(srcId);
if (it != m_grpAffTable.end()) {
m_grpAffTable.unlock();
return true;
}
m_grpAffTable.unlock();
return false;
}

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2022,2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022,2024,2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -264,6 +264,17 @@ namespace lookups
*/
void setDisableUnitRegTimeout(bool disabled) { m_disableUnitRegTimeout = disabled; }
/**
* @brief Helper to determine if the group affiliation timeout is enabled or not.
* @returns bool True, if idle group affiliation timeouts are disabled, otherwise false.
*/
virtual bool isDisableGrpAffTimeout() const { return m_disableGrpAffTimeout; }
/**
* @brief Disables the group affiliation timeout.
* @param disable Flag indicating idle group affiliation timeout should be disabled.
*/
void setDisableGrpAffTimeout(bool disabled) { m_disableGrpAffTimeout = disabled; }
/**
* @brief Helper to set the release grant callback.
* @note Do not call AffiliationLookup get functions from within this callback, deadlock protection
@ -285,6 +296,7 @@ namespace lookups
concurrent::vector<uint32_t> m_unitRegTable;
concurrent::unordered_map<uint32_t, Timer> m_unitRegTimers;
concurrent::unordered_map<uint32_t, uint32_t> m_grpAffTable;
concurrent::unordered_map<uint32_t, Timer> m_grpAffTimers;
concurrent::unordered_map<uint32_t, uint32_t> m_grantChTable;
concurrent::unordered_map<uint32_t, uint32_t> m_grantSrcIdTable;
@ -301,8 +313,16 @@ namespace lookups
ChannelLookup* m_chLookup;
bool m_disableUnitRegTimeout;
bool m_disableGrpAffTimeout;
bool m_verbose;
/**
* @brief Helper to determine if the source ID has group affiliations.
* @param srcId Source Radio ID.
* @returns bool True, if the source ID has group affiliations, otherwise false.
*/
bool isSrcIdGrpAff(uint32_t srcId) const;
};
} // namespace lookups

@ -250,7 +250,7 @@ bool BaseNetwork::announceGroupAffiliation(uint32_t srcId, uint32_t dstId)
SET_UINT24(srcId, buffer, 0U);
SET_UINT24(dstId, buffer, 3U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL }, buffer, MSG_ANNC_GRP_AFFIL, RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL }, buffer, MSG_ANNC_GRP_AFFIL, RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a group affiliation removal to the network. */
@ -264,7 +264,7 @@ bool BaseNetwork::announceGroupAffiliationRemoval(uint32_t srcId)
SET_UINT24(srcId, buffer, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL }, buffer, MSG_ANNC_GRP_UNAFFIL, RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL }, buffer, MSG_ANNC_GRP_UNAFFIL, RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a unit registration to the network. */
@ -278,7 +278,7 @@ bool BaseNetwork::announceUnitRegistration(uint32_t srcId)
SET_UINT24(srcId, buffer, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG }, buffer, MSG_ANNC_UNIT_REG, RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG }, buffer, MSG_ANNC_UNIT_REG, RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a unit deregistration to the network. */
@ -292,7 +292,7 @@ bool BaseNetwork::announceUnitDeregistration(uint32_t srcId)
SET_UINT24(srcId, buffer, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG }, buffer, MSG_ANNC_UNIT_REG, RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG }, buffer, MSG_ANNC_UNIT_REG, RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a complete update of the peer affiliation list to the network. */
@ -314,7 +314,28 @@ bool BaseNetwork::announceAffiliationUpdate(const std::unordered_map<uint32_t, u
offs += 8U;
}
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_AFFILS }, buffer, 4U + (affs.size() * 8U), RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_AFFILS }, buffer, 4U + (affs.size() * 8U), RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a complete update of the peer's unit registration list to the network. */
bool BaseNetwork::announceUnitRegUpdate(const std::vector<uint32_t> regs)
{
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return false;
DECLARE_UINT8_ARRAY(buffer, 4U + (regs.size() * 3U));
SET_UINT32(regs.size(), buffer, 0U);
// write unit IDs to active unit registration payload
uint32_t offs = 4U;
for (auto it : regs) {
SET_UINT24(it, buffer, offs);
offs += 3U;
}
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REGS }, buffer, 4U + (regs.size() * 3U), RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a complete update of the peer's voice channel list to the network. */
@ -335,7 +356,7 @@ bool BaseNetwork::announceSiteVCs(const std::vector<uint32_t> peers)
offs += 4U;
}
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC }, buffer, 4U + (peers.size() * 4U), RTP_END_OF_CALL_SEQ, 0U);
return writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC }, buffer, 4U + (peers.size() * 4U), RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Resets the DMR ring buffer for the given slot. */

@ -681,6 +681,27 @@ namespace network
*/
virtual bool announceAffiliationUpdate(const std::unordered_map<uint32_t, uint32_t> affs);
/**
* @brief Writes a complete update of the peer's unit registration list to the network.
* \code{.unparsed}
* Below is the representation of the data layout for the repeater/end point login message.
* The message is variable bytes in length.
*
* Each unit registration update entry is 3 bytes.
*
* Byte 0 1 2 3
* Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Number of entries |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Entry: Source ID |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* \endcode
* @param regs Complete list of unit registrations.
* @returns bool True, if unit registration update announcement was sent, otherwise false.
*/
virtual bool announceUnitRegUpdate(const std::vector<uint32_t> regs);
/**
* @brief Writes a complete update of the peer's voice channel list to the network.
* \code{.unparsed}

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023,2024,2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -106,6 +106,7 @@ namespace network
ANNC_SUBFUNC_UNIT_DEREG = 0x02U, //!< Announce Unit Deregistration
ANNC_SUBFUNC_GRP_UNAFFIL = 0x03U, //!< Announce Group Affiliation Removal
ANNC_SUBFUNC_AFFILS = 0x90U, //!< Update All Affiliations
ANNC_SUBFUNC_UNIT_REGS = 0x91U, //!< Update All Unit Registrations
ANNC_SUBFUNC_SITE_VC = 0x9AU, //!< Announce Site VCs
REPL_TALKGROUP_LIST = 0x00U, //!< FNE Replication Talkgroup Transfer

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
@ -192,6 +192,7 @@ void MetadataNetwork::taskNetworkRx(NetPacketRequest* req)
if (req->length > 0) {
uint32_t peerId = req->fneHeader.getPeerId();
uint32_t ssrc = req->rtpHeader.getSSRC();
uint32_t streamId = req->fneHeader.getStreamId();
// process incoming message function opcodes
@ -383,6 +384,319 @@ void MetadataNetwork::taskNetworkRx(NetPacketRequest* req)
}
break;
case NET_FUNC::ANNOUNCE: // Announce
{
// process incoming message subfunction opcodes
switch (req->fneHeader.getSubFunction()) {
case NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL: // Announce Group Affiliation
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
uint32_t dstId = GET_UINT24(req->buffer, 3U); // Destination Address
aff->groupUnaff(srcId);
aff->groupAff(srcId, dstId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG: // Announce Unit Registration
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->unitReg(srcId, ssrc);
network->m_globalAff->unitReg(srcId, ssrc);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true, 0U, ssrc);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG: // Announce Unit Deregistration
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->unitDereg(srcId);
network->m_globalAff->unitDereg(srcId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL: // Announce Group Affiliation Removal
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->groupUnaff(srcId);
network->m_globalAff->groupUnaff(srcId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_AFFILS: // Announce Update All Affiliations
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
if (aff != nullptr) {
aff->clearGroupAff(0U, true);
// update TGID lists
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t srcId = GET_UINT24(req->buffer, offs);
uint32_t dstId = GET_UINT24(req->buffer, offs + 4U);
aff->groupAff(srcId, dstId);
network->m_globalAff->groupAff(srcId, dstId);
offs += 8U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u affiliations", peerId, connection->identWithQualifier().c_str(), len);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_AFFILS },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REGS: // Announce Update All Unit Registrations
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
if (aff != nullptr) {
aff->clearUnitReg();
// update unit registration lists
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t srcId = GET_UINT24(req->buffer, offs);
aff->unitReg(srcId, ssrc);
network->m_globalAff->unitReg(srcId, ssrc);
offs += 3U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u unit registrations", peerId, connection->identWithQualifier().c_str(), len);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REGS },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC: // Announce Site VCs
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::vector<uint32_t> vcPeers;
// update peer association
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t vcPeerId = GET_UINT32(req->buffer, offs);
if (vcPeerId > 0 && (network->m_peers.find(vcPeerId) != network->m_peers.end())) {
FNEPeerConnection* vcConnection = network->m_peers[vcPeerId];
if (vcConnection != nullptr) {
vcConnection->ccPeerId(peerId);
vcPeers.push_back(vcPeerId);
}
}
offs += 4U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u VCs", peerId, connection->identWithQualifier().c_str(), len);
network->m_ccPeerMap[peerId] = vcPeers;
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
default:
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_ILLEGAL_PACKET);
Utils::dump("Unknown announcement opcode from the peer", req->buffer, req->length);
}
}
break;
case NET_FUNC::REPL:
if (req->fneHeader.getSubFunction() == NET_SUBFUNC::REPL_ACT_PEER_LIST) { // Peer Replication Active Peer List
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
@ -1878,268 +1878,11 @@ void TrafficNetwork::taskNetworkRx(NetPacketRequest* req)
case NET_FUNC::TRANSFER: // Transfer
// transfer command is not supported for performance reasons on the main traffic port
break;
case NET_FUNC::ANNOUNCE: // Announce
{
// process incoming message subfunction opcodes
switch (req->fneHeader.getSubFunction()) {
case NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL: // Announce Group Affiliation
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
uint32_t dstId = GET_UINT24(req->buffer, 3U); // Destination Address
aff->groupUnaff(srcId);
aff->groupAff(srcId, dstId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG: // Announce Unit Registration
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->unitReg(srcId, ssrc);
network->m_globalAff->unitReg(srcId, ssrc);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false, 0U, ssrc);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG: // Announce Unit Deregistration
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->unitDereg(srcId);
network->m_globalAff->unitDereg(srcId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL: // Announce Group Affiliation Removal
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->groupUnaff(srcId);
network->m_globalAff->groupUnaff(srcId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_AFFILS: // Announce Update All Affiliations
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
if (aff != nullptr) {
aff->clearGroupAff(0U, true);
// update TGID lists
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t srcId = GET_UINT24(req->buffer, offs);
uint32_t dstId = GET_UINT24(req->buffer, offs + 4U);
aff->groupAff(srcId, dstId);
network->m_globalAff->groupAff(srcId, dstId);
offs += 8U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u affiliations", peerId, connection->identWithQualifier().c_str(), len);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_AFFILS },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false);
}
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC: // Announce Site VCs
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::vector<uint32_t> vcPeers;
// update peer association
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t vcPeerId = GET_UINT32(req->buffer, offs);
if (vcPeerId > 0 && (network->m_peers.find(vcPeerId) != network->m_peers.end())) {
FNEPeerConnection* vcConnection = network->m_peers[vcPeerId];
if (vcConnection != nullptr) {
vcConnection->ccPeerId(peerId);
vcPeers.push_back(vcPeerId);
}
}
offs += 4U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u VCs", peerId, connection->identWithQualifier().c_str(), len);
network->m_ccPeerMap[peerId] = vcPeers;
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
default:
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_ILLEGAL_PACKET);
Utils::dump("Unknown announcement opcode from the peer", req->buffer, req->length);
}
}
break;
// bryanb: temporary support to allow announce packets on the traffic port but ultimately
// this should be removed and handled like TRANSFER is handled here
network->m_host->m_mdNetwork->taskNetworkRx(req);
return; // don't break, return because taskNetworkRx will cleanup req
default:
Utils::dump("Unknown opcode from the peer", req->buffer, req->length);
@ -2248,6 +1991,7 @@ void TrafficNetwork::createPeerAffiliations(uint32_t peerId, std::string peerNam
});
aff->setDisableUnitRegTimeout(true); // FNE doesn't allow unit registration timeouts (notification must come from the peers)
aff->setDisableGrpAffTimeout(true); // FNE doesn't allow group affiliation timeouts (notification must come from the peers)
m_peerAffiliations.insert(peerId, aff);
}

@ -152,6 +152,9 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa
m_slot1->setNotifyCC(notifyCC);
m_slot2->setNotifyCC(notifyCC);
bool disableGrpAffTimeout = dmrProtocol["disableGrpAffTimeout"].as<bool>(false);
m_slot1->s_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout);
m_slot2->s_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout);
bool disableUnitRegTimeout = dmrProtocol["disableUnitRegTimeout"].as<bool>(false);
m_slot1->s_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
m_slot2->s_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
@ -231,6 +234,14 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa
LogInfo(" Default Network Idle Talkgroup: %u", defaultNetIdleTalkgroup);
}
if (disableGrpAffTimeout) {
LogInfo(" Disable Group Affiliation Timeout: yes");
}
if (disableUnitRegTimeout) {
LogInfo(" Disable Unit Registration Timeout: yes");
}
LogInfo(" Ignore Affiliation Check: %s", ignoreAffiliationCheck ? "yes" : "no");
LogInfo(" Legacy Group Registration: %s", legacyGroupReg ? "yes" : "no");
LogInfo(" Notify Control: %s", notifyCC ? "yes" : "no");

@ -813,6 +813,13 @@ void Slot::clockSiteData(uint32_t ms)
if (m_rfState == RS_RF_LISTENING && m_netState == RS_NET_IDLE) {
m_control->writeAdjSSNetwork();
if (s_network != nullptr) {
// network announce our unit registration table if we have one
if (s_affiliations->unitRegSize() > 0) {
auto regs = s_affiliations->unitRegTable();
s_network->announceUnitRegUpdate(regs);
}
// network announce our affiliation table if we have one
if (s_affiliations->grpAffSize() > 0) {
auto affs = s_affiliations->grpAffTable();
s_network->announceAffiliationUpdate(affs);

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015-2020 Jonathan Naylor, G4KLX
* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -274,6 +274,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
m_controlChData = controlChData;
bool disableGrpAffTimeout = nxdnProtocol["disableGrpAffTimeout"].as<bool>(false);
m_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout);
bool disableUnitRegTimeout = nxdnProtocol["disableUnitRegTimeout"].as<bool>(false);
m_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
@ -342,6 +344,10 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
LogInfo(" Verify Affiliation: %s", m_control->m_verifyAff ? "yes" : "no");
LogInfo(" Verify Registration: %s", m_control->m_verifyReg ? "yes" : "no");
if (disableGrpAffTimeout) {
LogInfo(" Disable Group Affiliation Timeout: yes");
}
if (disableUnitRegTimeout) {
LogInfo(" Disable Unit Registration Timeout: yes");
}
@ -634,6 +640,13 @@ void Control::clock()
if (m_adjSiteUpdate.isRunning() && m_adjSiteUpdate.hasExpired()) {
if (m_rfState == RS_RF_LISTENING && m_netState == RS_NET_IDLE) {
if (m_network != nullptr) {
// network announce our unit registration table if we have one
if (m_affiliations->unitRegSize() > 0) {
auto regs = m_affiliations->unitRegTable();
m_network->announceUnitRegUpdate(regs);
}
// network announce our affiliation table if we have one
if (m_affiliations->grpAffSize() > 0) {
auto affs = m_affiliations->grpAffTable();
m_network->announceAffiliationUpdate(affs);

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016,2017,2018 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -458,6 +458,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
m_controlChData = controlChData;
bool disableGrpAffTimeout = p25Protocol["disableGrpAffTimeout"].as<bool>(false);
m_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout);
bool disableUnitRegTimeout = p25Protocol["disableUnitRegTimeout"].as<bool>(false);
m_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
@ -573,6 +575,10 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
LogInfo(" Notify VCs of Active TGs: %s", m_ccNotifyActiveTG ? "yes" : "no");
if (disableGrpAffTimeout) {
LogInfo(" Disable Group Affiliation Timeout: yes");
}
if (disableUnitRegTimeout) {
LogInfo(" Disable Unit Registration Timeout: yes");
}
@ -1100,6 +1106,13 @@ void Control::clockSiteData(uint32_t ms)
if (m_rfState == RS_RF_LISTENING && m_netState == RS_NET_IDLE) {
m_control->writeAdjSSNetwork();
if (m_network != nullptr) {
// network announce our unit registration table if we have one
if (m_affiliations->unitRegSize() > 0) {
auto regs = m_affiliations->unitRegTable();
m_network->announceUnitRegUpdate(regs);
}
// network announce our affiliation table if we have one
if (m_affiliations->grpAffSize() > 0) {
auto affs = m_affiliations->grpAffTable();
m_network->announceAffiliationUpdate(affs);

Loading…
Cancel
Save

Powered by TurnKey Linux.