diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index d35e7ca0..6cb74764 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -108,6 +108,8 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q m_netLastDstId(0U), m_netLastSrcId(0U), m_permittedDstId(0U), + m_permittedSrcId(0U), + m_permittedU2U(false), m_permittedTelInt(false), m_tailOnIdle(false), m_ccRunning(false), @@ -220,6 +222,10 @@ void Control::reset() m_data->resetRF(); } + if (m_control != nullptr) { + m_control->resetU2UAvailabilityTransactions(); + } + m_txImmQueue.clear(); m_txQueue.clear(); } @@ -925,6 +931,7 @@ void Control::clock() } // handle timeouts and hang timers + m_control->clock(ms); m_rfTimeoutTimer.clock(ms); m_netTimeoutTimer.clock(ms); m_rfVoiceCallTermTimeout.clock(ms); @@ -947,9 +954,9 @@ void Control::clock() m_rfLastDstId = 0U; m_rfLastSrcId = 0U; - // reset permitted ID and clear permission state (a telephone interconnect permit is held for the - // whole call and only cleared by the control channel) - if (!m_authoritative && m_permittedDstId != 0U && !m_permittedTelInt) { + // message-trunked U2U and telephone interconnect permits are held + // for the whole call and only cleared by the control channel + if (!m_authoritative && m_permittedDstId != 0U && !m_permittedTelInt && !m_permittedU2U) { m_permittedDstId = 0U; } @@ -1262,7 +1269,7 @@ void Control::clockSiteData(uint32_t ms) /* Permits a TGID on a non-authoritative host. */ -void Control::permittedTG(uint32_t dstId, bool dataPermit, bool telInt) +void Control::permittedTG(uint32_t dstId, bool dataPermit, bool telInt, uint32_t srcId, bool unitToUnit) { if (m_authoritative) { return; @@ -1272,7 +1279,8 @@ void Control::permittedTG(uint32_t dstId, bool dataPermit, bool telInt) if (dstId == 0U) LogInfoEx(LOG_P25, "non-authoritative TG unpermit"); else - LogInfoEx(LOG_P25, "non-authoritative TG permit, dstId = %u, telInt = %u", dstId, telInt); + LogInfoEx(LOG_P25, "non-authoritative TG permit, srcId = %u, dstId = %u, u2u = %u, telInt = %u", + srcId, dstId, unitToUnit, telInt); } // if this a unpermit ensure we write TDUs @@ -1287,6 +1295,8 @@ void Control::permittedTG(uint32_t dstId, bool dataPermit, bool telInt) } m_permittedDstId = dstId; + m_permittedSrcId = (dstId != 0U && unitToUnit) ? srcId : 0U; + m_permittedU2U = (dstId != 0U) && unitToUnit; m_permittedTelInt = (dstId != 0U) && telInt; // is this a data permit? @@ -1295,6 +1305,21 @@ void Control::permittedTG(uint32_t dstId, bool dataPermit, bool telInt) } } +/* Tests traffic identities against the current voice-channel permit. */ + +bool Control::isPermitted(uint32_t srcId, uint32_t dstId, bool group) const +{ + if (m_authoritative) { + return true; + } + + if (m_permittedU2U) { + return !group && ((srcId == m_permittedSrcId && dstId == m_permittedDstId) || (srcId == m_permittedDstId && dstId == m_permittedSrcId)); + } + + return dstId == m_permittedDstId; +} + /* Grants a TGID on a non-authoritative host. */ void Control::grantTG(uint32_t srcId, uint32_t dstId, bool grp) @@ -2148,9 +2173,19 @@ void Control::RPC_permittedTG(json::object& req, json::object& reply) telInt = (bool)req["telInt"].get(); } + uint32_t srcId = 0U; + if (req["srcId"].is()) { + srcId = req["srcId"].get(); + } + + bool unitToUnit = false; + if (req["unitToUnit"].is()) { + unitToUnit = req["unitToUnit"].get(); + } + // LogDebugEx(LOG_P25, "Control::RPC_permittedTG()", "callback, dstId = %u, dataPermit = %u", dstId, dataPermit); - permittedTG(dstId, dataPermit, telInt); + permittedTG(dstId, dataPermit, telInt, srcId, unitToUnit); } /* (RPC Handler) Active TGID list from the authoritative CC host. */ @@ -2226,17 +2261,7 @@ void Control::RPC_releaseGrantTG(json::object& req, json::object& reply) LogInfoEx(LOG_P25, "VC request, release TG grant, dstId = %u", dstId); } - if (m_affiliations->isGranted(dstId)) { - uint32_t chNo = m_affiliations->getGrantedCh(dstId); - uint32_t srcId = m_affiliations->getGrantedSrcId(dstId); - ::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo); - - if (m_verbose) { - LogInfoEx(LOG_P25, "VC %s:%u, TG grant released, srcId = %u, dstId = %u, chNo = %u-%u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo); - } - - m_affiliations->releaseGrant(dstId, false); - } + releaseGrantFromVoiceChannel(dstId); } /* (RPC Handler) Touches a granted TG to keep a channel grant alive. */ @@ -2265,17 +2290,60 @@ void Control::RPC_touchGrantTG(json::object& req, json::object& reply) // LogDebugEx(LOG_P25, "Control::RPC_touchGrantTG()", "callback, dstId = %u", dstId); - if (m_affiliations->isGranted(dstId)) { - uint32_t chNo = m_affiliations->getGrantedCh(dstId); - uint32_t srcId = m_affiliations->getGrantedSrcId(dstId); + uint32_t grantOwner = dstId; + if (!m_affiliations->isGranted(grantOwner)) { + grantOwner = m_affiliations->getU2UGrantOwner(dstId); + } + + if (m_affiliations->isGranted(grantOwner)) { + uint32_t chNo = m_affiliations->getGrantedCh(grantOwner); + uint32_t srcId = m_affiliations->getGrantedSrcId(grantOwner); ::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo); if (m_verbose) { LogInfoEx(LOG_P25, "VC %s:%u, call in progress, srcId = %u, dstId = %u, chNo = %u-%u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo); } - m_affiliations->touchGrant(dstId); + m_affiliations->touchGrant(grantOwner); + } +} + +/* Handles a voice-channel request to release or retain a completed grant. */ + +void Control::releaseGrantFromVoiceChannel(uint32_t dstId) +{ + uint32_t grantOwner = dstId; + if (!m_affiliations->isGranted(grantOwner)) { + // the VC reports the current directional target, which may not be the + // destination-key owner of the private grant + grantOwner = m_affiliations->getU2UGrantOwner(dstId); + } + + if (!m_affiliations->isGranted(grantOwner)) { + return; + } + + uint32_t chNo = m_affiliations->getGrantedCh(grantOwner); + uint32_t srcId = m_affiliations->getGrantedSrcId(grantOwner); + ::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo); + + if (!m_affiliations->isGroup(grantOwner)) { + // U2U calls are message-trunked; retain the assignment for the grant + // hang timer so the other participant can continue on the same channel + if (m_verbose) { + LogInfoEx(LOG_P25, "VC %s:%u, U2U grant retained for call continuation, srcId = %u, dstId = %u, chNo = %u-%u", + voiceCh.address().c_str(), voiceCh.port(), srcId, grantOwner, voiceCh.chId(), chNo); + } + + m_affiliations->touchGrant(grantOwner); + return; } + + if (m_verbose) { + LogInfoEx(LOG_P25, "VC %s:%u, TG grant released, srcId = %u, dstId = %u, chNo = %u-%u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo); + } + + m_affiliations->releaseGrant(grantOwner, false); } /* Helper to retrieve LLA AM1 parameters for a given source ID. */ diff --git a/src/host/p25/Control.h b/src/host/p25/Control.h index 71322c04..1d73e08b 100644 --- a/src/host/p25/Control.h +++ b/src/host/p25/Control.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016,2017 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL * */ /** @@ -215,7 +215,16 @@ namespace p25 * @param dataPermit Flag indicating this permit is for data. * @param telInt Flag indicating this permit is for a telephone interconnect call to the unit in dstId. */ - void permittedTG(uint32_t dstId, bool dataPermit = false, bool telInt = false); + void permittedTG(uint32_t dstId, bool dataPermit = false, bool telInt = false, + uint32_t srcId = 0U, bool unitToUnit = false); + /** + * @brief Tests traffic identities against the current voice-channel permit. + * @param srcId Source radio ID. + * @param dstId Destination ID. + * @param group Flag indicating group traffic. + * @returns bool True when the traffic matches the active permit. + */ + bool isPermitted(uint32_t srcId, uint32_t dstId, bool group) const; /** * @brief Grants a TGID on a non-authoritative host. * @param srcId Source Radio ID. @@ -358,6 +367,8 @@ namespace p25 uint32_t m_netLastSrcId; uint32_t m_permittedDstId; + uint32_t m_permittedSrcId; + bool m_permittedU2U; bool m_permittedTelInt; bool m_tailOnIdle; @@ -527,6 +538,12 @@ namespace p25 void RPC_touchGrantTG(json::object& req, json::object& reply); /** @} */ + /** + * @brief Handles a voice-channel request to release or retain a completed grant. + * @param dstId Destination-keyed grant owner reported by the voice channel. + */ + void releaseGrantFromVoiceChannel(uint32_t dstId); + /** * @brief Helper to retrieve LLA AM1 parameters for a given source ID. * @param srcId Source ID. diff --git a/src/host/p25/lookups/P25AffiliationLookup.cpp b/src/host/p25/lookups/P25AffiliationLookup.cpp index 72dda5e5..84ad7893 100644 --- a/src/host/p25/lookups/P25AffiliationLookup.cpp +++ b/src/host/p25/lookups/P25AffiliationLookup.cpp @@ -4,13 +4,15 @@ * 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 * */ #include "common/Log.h" #include "p25/lookups/P25AffiliationLookup.h" #include "p25/Control.h" +#include + using namespace p25::lookups; // --------------------------------------------------------------------------- @@ -29,6 +31,50 @@ P25AffiliationLookup::P25AffiliationLookup(Control* p25, ::lookups::ChannelLooku P25AffiliationLookup::~P25AffiliationLookup() = default; +/* Produces the stable unordered key for a private-call RID pair. */ + +uint64_t P25AffiliationLookup::u2uPairKey(uint32_t srcId, uint32_t dstId) +{ + if (srcId == 0U || dstId == 0U || srcId == dstId) { + return 0U; + } + + const uint32_t low = std::min(srcId, dstId) & 0xFFFFFFU; + const uint32_t high = std::max(srcId, dstId) & 0xFFFFFFU; + return (static_cast(low) << 24) | high; +} + +/* Grants a channel and records private-call participant identity. */ + +bool P25AffiliationLookup::grantCh(uint32_t dstId, uint32_t srcId, uint32_t grantTimeout, bool grp, bool netGranted) +{ + if (!grp && isU2UGranted(srcId, dstId)) { + return false; + } + + if (!grp) { + for (const auto& entry : m_u2uGrantOwners) { + const uint32_t low = static_cast((entry.first >> 24) & 0xFFFFFFU); + const uint32_t high = static_cast(entry.first & 0xFFFFFFU); + if (low == srcId || low == dstId || high == srcId || high == dstId) { + return false; + } + } + } + + const bool granted = ::lookups::AffiliationLookup::grantCh(dstId, srcId, grantTimeout, grp, netGranted); + if (granted && !grp) { + const uint64_t key = u2uPairKey(srcId, dstId); + if (key != 0U) { + m_u2uGrantOwners[key] = dstId; + m_u2uGrantSources[dstId] = srcId; + m_u2uGrantTargets[dstId] = dstId; + } + } + + return granted; +} + /* Helper to release group affiliations. */ std::vector P25AffiliationLookup::clearGroupAff(uint32_t dstId, bool releaseAll) @@ -48,8 +94,39 @@ std::vector P25AffiliationLookup::clearGroupAff(uint32_t dstId, bool r bool P25AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll) { - bool ret = ::lookups::AffiliationLookup::releaseGrant(dstId, releaseAll); + uint32_t grantOwner = dstId; + if (!releaseAll && !::lookups::AffiliationLookup::isGranted(grantOwner)) { + for (const auto& entry : m_u2uGrantOwners) { + const uint32_t low = static_cast((entry.first >> 24) & 0xFFFFFFU); + const uint32_t high = static_cast(entry.first & 0xFFFFFFU); + if (low == dstId || high == dstId) { + grantOwner = entry.second; + break; + } + } + } + + bool ret = ::lookups::AffiliationLookup::releaseGrant(grantOwner, releaseAll); if (ret) { + if (releaseAll) { + m_u2uGrantOwners.clear(); + m_u2uGrantSources.clear(); + m_u2uGrantTargets.clear(); + } + else { + for (auto it = m_u2uGrantOwners.begin(); it != m_u2uGrantOwners.end(); ) { + if (it->second == grantOwner) { + it = m_u2uGrantOwners.erase(it); + } + else { + ++it; + } + } + + m_u2uGrantSources.erase(grantOwner); + m_u2uGrantTargets.erase(grantOwner); + } + if (m_rfGrantChCnt > 0U) { m_p25->m_siteData.setChCnt(m_chLookup->rfChSize() + m_rfGrantChCnt); } @@ -60,3 +137,80 @@ bool P25AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll) return ret; } + +/* Resolves a unit-to-unit participant pair to its single grant owner. */ + +uint32_t P25AffiliationLookup::getU2UGrantOwner(uint32_t srcId, uint32_t dstId) const +{ + const uint64_t key = u2uPairKey(srcId, dstId); + auto it = m_u2uGrantOwners.find(key); + if (it == m_u2uGrantOwners.end()) { + return 0U; + } + + return ::lookups::AffiliationLookup::isGranted(it->second) ? it->second : 0U; +} + +/* Resolves one private-call participant to its grant owner. */ + +uint32_t P25AffiliationLookup::getU2UGrantOwner(uint32_t unitId) const +{ + for (const auto& entry : m_u2uGrantOwners) { + const uint32_t low = static_cast((entry.first >> 24) & 0xFFFFFFU); + const uint32_t high = static_cast(entry.first & 0xFFFFFFU); + if (low == unitId || high == unitId) { + return ::lookups::AffiliationLookup::isGranted(entry.second) ? entry.second : 0U; + } + } + + return 0U; +} + +/* Releases the active unit-to-unit grant involving a participant. */ + +bool P25AffiliationLookup::releaseU2UGrant(uint32_t unitId) +{ + const uint32_t grantOwner = getU2UGrantOwner(unitId); + return grantOwner != 0U && releaseGrant(grantOwner, false); +} + +/* Updates the directional signaling identities for an active private grant. */ + +bool P25AffiliationLookup::setU2UGrantDirection(uint32_t srcId, uint32_t dstId) +{ + const uint32_t grantOwner = getU2UGrantOwner(srcId, dstId); + if (grantOwner == 0U) { + return false; + } + + m_u2uGrantSources[grantOwner] = srcId; + m_u2uGrantTargets[grantOwner] = dstId; + return true; +} + +/* Gets the current directional source RID for a private grant owner. */ + +uint32_t P25AffiliationLookup::getU2UGrantSource(uint32_t grantOwner) const +{ + auto it = m_u2uGrantSources.find(grantOwner); + return (it != m_u2uGrantSources.end()) ? it->second : 0U; +} + +/* Gets the current directional target RID for a private grant owner. */ + +uint32_t P25AffiliationLookup::getU2UGrantTarget(uint32_t grantOwner) const +{ + auto it = m_u2uGrantTargets.find(grantOwner); + return (it != m_u2uGrantTargets.end()) ? it->second : 0U; +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Tests whether a unit-to-unit participant pair has an active grant. */ + +bool P25AffiliationLookup::isU2UGranted(uint32_t srcId, uint32_t dstId) const +{ + return getU2UGrantOwner(srcId, dstId) != 0U; +} diff --git a/src/host/p25/lookups/P25AffiliationLookup.h b/src/host/p25/lookups/P25AffiliationLookup.h index 56540b6a..b92e0d4a 100644 --- a/src/host/p25/lookups/P25AffiliationLookup.h +++ b/src/host/p25/lookups/P25AffiliationLookup.h @@ -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 * */ /** @@ -24,6 +24,8 @@ #include "common/lookups/AffiliationLookup.h" #include "common/lookups/ChannelLookup.h" +#include + namespace p25 { // --------------------------------------------------------------------------- @@ -68,6 +70,16 @@ namespace p25 /** @} */ /** @name Channel Grants */ + /** + * @brief Grants a channel and records private-call participant identity. + * @param dstId Destination address. + * @param srcId Source radio ID. + * @param grantTimeout Time before the grant expires from inactivity. + * @param grp Flag indicating the grant is for a talkgroup. + * @param netGranted Flag indicating the grant came from network traffic. + * @returns bool True if the grant was created; otherwise, false. + */ + bool grantCh(uint32_t dstId, uint32_t srcId, uint32_t grantTimeout, bool grp, bool netGranted) override; /** * @brief Helper to release the channel grant for the destination ID. * @param dstId Destination Address. @@ -75,10 +87,68 @@ namespace p25 * @returns bool True, if channel grant was released, otherwise false. */ bool releaseGrant(uint32_t dstId, bool releaseAll) override; + + /** + * @brief Resolves a unit-to-unit participant pair to its single grant owner. + * @param srcId One participant RID. + * @param dstId The other participant RID. + * @returns uint32_t Destination-key owner of the grant, or zero when absent. + */ + uint32_t getU2UGrantOwner(uint32_t srcId, uint32_t dstId) const; + /** + * @brief Resolves one private-call participant to its grant owner. + * @param unitId RID of either private-call participant. + * @returns uint32_t Destination-key owner of the grant, or zero when absent. + */ + uint32_t getU2UGrantOwner(uint32_t unitId) const; + /** + * @brief Releases the active unit-to-unit grant involving a participant. + * @param unitId RID of either private-call participant. + * @returns bool True when an active private grant was released. + */ + bool releaseU2UGrant(uint32_t unitId); + /** + * @brief Updates the directional signaling identities for an active private grant. + * @param srcId Current first-to-talk/source RID. + * @param dstId Current target RID. + * @returns bool True when the active pair was updated. + */ + bool setU2UGrantDirection(uint32_t srcId, uint32_t dstId); + /** + * @brief Gets the current directional source RID for a private grant owner. + * @param grantOwner Destination-key owner of the underlying grant. + * @returns uint32_t Current source RID, or zero when unavailable. + */ + uint32_t getU2UGrantSource(uint32_t grantOwner) const; + /** + * @brief Gets the current directional target RID for a private grant owner. + * @param grantOwner Destination-key owner of the underlying grant. + * @returns uint32_t Current target RID, or zero when unavailable. + */ + uint32_t getU2UGrantTarget(uint32_t grantOwner) const; + /** + * @brief Tests whether a unit-to-unit participant pair has an active grant. + * @param srcId One participant RID. + * @param dstId The other participant RID. + * @returns bool True if the pair has one active private grant. + */ + bool isU2UGranted(uint32_t srcId, uint32_t dstId) const; /** @} */ protected: Control* m_p25; + + std::unordered_map m_u2uGrantOwners; //!< Maps an unordered private-call RID pair to one destination-key grant owner. + std::unordered_map m_u2uGrantSources; //!< Maps a private grant owner to the current first-to-talk/source RID. + std::unordered_map m_u2uGrantTargets; //!< Maps a private grant owner to the current target RID. + + /** + * @brief Produces the stable unordered key for a private-call RID pair. + * @param srcId One participant RID. + * @param dstId The other participant RID. + * @returns uint64_t Stable 48-bit pair key, or zero for an invalid pair. + */ + static uint64_t u2uPairKey(uint32_t srcId, uint32_t dstId); }; } // namespace lookups } // namespace p25 diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index ec32b838..bce52dc3 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -152,12 +152,14 @@ const uint32_t ADJ_SITE_UPDATE_CNT = 5U; const uint32_t TSDU_CTRL_BURST_COUNT = 2U; const uint32_t TSBK_MBF_CNT = 3U; const uint32_t GRANT_TIMER_TIMEOUT = 15U; +const uint32_t U2U_AVAILABILITY_TIMER_TIMEOUT = 15U; const uint8_t CONV_FALLBACK_PACKET_DELAY = 8U; // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- + /* Process a data frame from the RF interface. */ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr preDecodedTSBK) @@ -257,15 +259,39 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrtoString(true), TSBKO::IOSP_UU_VCH, srcId); VERBOSE_LOG_TSBK(tsbk->toString(true), srcId, dstId); + const uint8_t serviceOptions = (tsbk->getEmergency() ? 0x80U : 0x00U) + + (tsbk->getEncrypted() ? 0x40U : 0x00U) + + (tsbk->getPriority() & 0x07U); + + if (m_p25->m_affiliations->isU2UGranted(srcId, dstId)) { + // Control-channel continuation of an active message-trunked U2U call; + // address the existing assignment to the requesting SU and do not + // restart target availability checking + m_u2uAvailabilityTransactions.erase(u2uTransactionKey(srcId, dstId)); + m_u2uAvailabilityTransactions.erase(u2uTransactionKey(dstId, srcId)); + if (m_p25->m_authoritative) { + // TIA-102.AABD-B 4.2.1.2.2 and 7.3.6: the requesting SU + // is first to talk and therefore occupies the grant source field + writeRF_TSDU_Grant(srcId, dstId, serviceOptions, false, + false, false, 0U, false, srcId); + } + else if (m_p25->m_network != nullptr) { + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, + srcId, dstId, 0U, true); + } + break; + } + if (m_unitToUnitAvailCheck) { + startU2UAvailabilityTransaction(srcId, dstId, serviceOptions, true, true); writeRF_TSDU_UU_Ans_Req(srcId, dstId); } else { + // some SUs use manual-answer signaling even when the FNE grants an unchecked + // U2U request immediately; retain correlation state so a later PROCEED can + // reissue a grant that expired while the called unit was ringing + startU2UAvailabilityTransaction(srcId, dstId, serviceOptions, true, false); if (m_p25->m_authoritative) { - uint8_t serviceOptions = (tsbk->getEmergency() ? 0x80U : 0x00U) + // Emergency Flag - (tsbk->getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag - (tsbk->getPriority() & 0x07U); // Priority - writeRF_TSDU_Grant(srcId, dstId, serviceOptions, false); } else { if (m_p25->m_network != nullptr) @@ -291,24 +317,60 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrtoString(true).c_str(), iosp->getResponse(), srcId, dstId); } + // look up the pending unit-to-unit availability transaction for this source and destination ID + const uint64_t transactionKey = u2uTransactionKey(dstId, srcId); + auto transactionIt = m_u2uAvailabilityTransactions.find(transactionKey); + if (transactionIt == m_u2uAvailabilityTransactions.end()) { + LogWarning(LOG_RF, P25_TSDU_STR ", %s ignored, no matching pending request, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), srcId, dstId); + // preserve the received ISP response layout for a remote originating site + writeNetworkRF(iosp, data + 2U, true); + break; + } + + // retrieve the pending transaction for this unit-to-unit request + U2UAvailabilityTransaction& transaction = transactionIt->second; + if (transaction.responseTimer.hasExpired()) { + LogWarning(LOG_RF, P25_TSDU_STR ", %s ignored, pending request expired, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), srcId, dstId); + m_u2uAvailabilityTransactions.erase(transactionIt); + writeNetworkRF(iosp, data + 2U, true); + break; + } + + // handle the response for the unit-to-unit availability request if (iosp->getResponse() == ResponseCode::ANS_PROCEED) { if (m_p25->m_authoritative) { - uint8_t serviceOptions = (tsbk->getEmergency() ? 0x80U : 0x00U) + // Emergency Flag - (tsbk->getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag - (tsbk->getPriority() & 0x07U); // Priority - - writeRF_TSDU_Grant(srcId, dstId, serviceOptions, false); + // select the answering unit as first talker and address the grant + // to the original caller so both subscribers follow the assignment + writeRF_TSDU_Grant(srcId, dstId, transaction.serviceOptions, false, false, false, 0U, false, srcId); } else { if (m_p25->m_network != nullptr) - m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, true); + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, + srcId, dstId, 0U, true); } + + m_u2uAvailabilityTransactions.erase(transactionIt); } else if (iosp->getResponse() == ResponseCode::ANS_DENY) { + // handle the denial response for the unit-to-unit availability request writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_UNIT_REFUSED, TSBKO::IOSP_UU_ANS); + m_u2uAvailabilityTransactions.erase(transactionIt); } else if (iosp->getResponse() == ResponseCode::ANS_WAIT) { + // handle the wait response for the unit-to-unit availability request writeRF_TSDU_Queue(srcId, dstId, ReasonCode::QUE_TGT_UNIT_QUEUED, TSBKO::IOSP_UU_ANS); + transaction.state = U2UAvailabilityState::WAITING; + transaction.responseTimer.start(); + } + else { + LogWarning(LOG_RF, P25_TSDU_STR ", %s ignored, invalid response = $%02X, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), iosp->getResponse(), srcId, dstId); } + + // preserve and forward the received ISP response; re-encoding + // IOSP_UU_ANS would incorrectly produce the OSP request layout + writeNetworkRF(iosp, data + 2U, true); } break; case TSBKO::ISP_TELE_INT_PSTN_REQ: @@ -534,6 +596,21 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrsecond.callerId == srcId || it->second.calledId == srcId) { + it = m_u2uAvailabilityTransactions.erase(it); + } + else { + ++it; + } + } + + m_p25->m_affiliations->releaseU2UGrant(srcId); + } + writeRF_TSDU_ACK_FNE(srcId, isp->getService(), false, true); } break; @@ -925,9 +1002,60 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, response = $%02X, srcId = %u, dstId = %u", tsbk->toString(true).c_str(), iosp->getResponse(), srcId, dstId); } + + // look up the pending U2U availability transaction for this response + const uint64_t transactionKey = u2uTransactionKey(dstId, srcId); + auto transactionIt = m_u2uAvailabilityTransactions.find(transactionKey); + if (transactionIt == m_u2uAvailabilityTransactions.end() || + transactionIt->second.responseTimer.hasExpired()) { + if (transactionIt != m_u2uAvailabilityTransactions.end()) { + m_u2uAvailabilityTransactions.erase(transactionIt); + } + + LogWarning(LOG_NET, P25_TSDU_STR ", %s ignored, no matching pending request, srcId = %u, dstId = %u", + tsbk->toString(true).c_str(), srcId, dstId); + return true; + } + + // process the response based on the received ISP code + U2UAvailabilityTransaction& transaction = transactionIt->second; + + // handle the response based on the ISP code + if (iosp->getResponse() == ResponseCode::ANS_PROCEED) { + if (m_p25->m_authoritative) { + // select the answering unit as first talker and address the grant + // to the original caller so both subscribers follow the assignment + writeRF_TSDU_Grant(srcId, dstId, transaction.serviceOptions, false, false, false, 0U, false, srcId); + } + else if (m_p25->m_network != nullptr) { + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, + srcId, dstId, 0U, true); + } + + m_u2uAvailabilityTransactions.erase(transactionIt); + } + else if (iosp->getResponse() == ResponseCode::ANS_DENY) { + // handle the denial response by sending a deny packet to the source ID + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_UNIT_REFUSED, TSBKO::IOSP_UU_ANS); + m_u2uAvailabilityTransactions.erase(transactionIt); + } + else if (iosp->getResponse() == ResponseCode::ANS_WAIT) { + // handle the wait response by sending a queue packet to the source ID + writeRF_TSDU_Queue(srcId, dstId, ReasonCode::QUE_TGT_UNIT_QUEUED, TSBKO::IOSP_UU_ANS); + transaction.state = U2UAvailabilityState::WAITING; + transaction.responseTimer.start(); + } + + return true; } else { VERBOSE_LOG_TSBK_NET(tsbk->toString(), srcId, dstId); + if (m_p25->m_affiliations->isUnitReg(dstId)) { + const uint8_t serviceOptions = (tsbk->getEmergency() ? 0x80U : 0x00U) + + (tsbk->getEncrypted() ? 0x40U : 0x00U) + + (tsbk->getPriority() & 0x07U); + startU2UAvailabilityTransaction(srcId, dstId, serviceOptions, false, true); + } } } break; @@ -1423,6 +1551,38 @@ ControlSignaling::~ControlSignaling() delete[] m_rfMBF; } +/* Updates and expires pending unit-to-unit availability transactions. */ + +void ControlSignaling::clock(uint32_t ms) +{ + // update and expire pending unit-to-unit availability transactions based on the response timer + for (auto it = m_u2uAvailabilityTransactions.begin(); it != m_u2uAvailabilityTransactions.end(); ) { + it->second.responseTimer.clock(ms); + if (it->second.responseTimer.isRunning() && it->second.responseTimer.hasExpired()) { + if (it->second.answerRequired) { + LogWarning(LOG_RF, P25_TSDU_STR ", unit-to-unit availability response expired, srcId = %u, dstId = %u", it->second.callerId, it->second.calledId); + } + + // if the transaction requires an answer and originated from a site, send a deny response to the called ID + if (it->second.answerRequired && it->second.originatingSite) { + writeRF_TSDU_Deny(it->second.calledId, it->second.callerId, ReasonCode::DENY_TGT_UNIT_REFUSED, TSBKO::IOSP_UU_VCH); + } + + it = m_u2uAvailabilityTransactions.erase(it); + } + else { + ++it; + } + } +} + +/* Clears all pending unit-to-unit availability transactions. */ + +void ControlSignaling::resetU2UAvailabilityTransactions() +{ + m_u2uAvailabilityTransactions.clear(); +} + /* Write data processed from RF to the network. */ void ControlSignaling::writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool autoReset) @@ -2235,7 +2395,7 @@ void ControlSignaling::queueRF_TSBK_Ctrl(uint8_t lco) /* Helper to write a grant packet. */ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp, bool net, bool skip, uint32_t chNo, - bool telInt) + bool telInt, uint32_t u2uConfirmedUnitId) { bool emergency = ((serviceOptions & 0xFFU) & 0x80U) == 0x80U; // Emergency Flag bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag @@ -2346,7 +2506,9 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } - if (!m_p25->m_affiliations->isGranted(dstId)) { + const uint32_t u2uGrantOwner = (!grp) ? m_p25->m_affiliations->getU2UGrantOwner(srcId, dstId) : 0U; + const bool alreadyGranted = (grp) ? m_p25->m_affiliations->isGranted(dstId) : u2uGrantOwner != 0U; + if (!alreadyGranted) { if (grp && !m_p25->m_ignoreAffiliationCheck) { // is this an affiliation required group? ::lookups::TalkgroupRuleGroupVoice tid = m_p25->m_tidLookup->find(dstId); @@ -2378,10 +2540,9 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } - if (!grp && !m_p25->m_ignoreAffiliationCheck && m_unitToUnitAvailCheck) { - // is this the target registered? + if (!grp && !m_p25->m_ignoreAffiliationCheck && m_unitToUnitAvailCheck && u2uConfirmedUnitId == 0U) { if (!m_p25->m_affiliations->isUnitReg(dstId)) { - LogWarning(LOG_NET, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request) ignored, no unit registration, dstId = %u", dstId); + LogWarning(LOG_NET, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request) ignored, no unit registration, unitId = %u", dstId); return false; } } @@ -2414,7 +2575,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ if (grp) { if (!net) { LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denied, no channels available, dstId = %u", dstId); - writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_NO_RF_RSRC_AVAIL, TSBKO::IOSP_GRP_VCH, grp, true); + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_NO_RF_RSRC_AVAIL, TSBKO::IOSP_UU_VCH, grp, true); ::ActivityLog("P25", true, "group grant request from %u to TG %u denied", srcId, dstId); m_p25->m_rfState = RS_RF_REJECTED; @@ -2448,7 +2609,8 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } else { - if (!m_disableGrantSrcIdCheck) { + const uint32_t grantOwner = (grp) ? dstId : u2uGrantOwner; + if (grp && !m_disableGrantSrcIdCheck) { // do collision check between grants to see if a SU is attempting a "grant retry" or if this is a // different source from the original grant uint32_t grantedSrcId = m_p25->m_affiliations->getGrantedSrcId(dstId); @@ -2468,8 +2630,8 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } - chNo = m_p25->m_affiliations->getGrantedCh(dstId); - m_p25->m_affiliations->touchGrant(dstId); + chNo = m_p25->m_affiliations->getGrantedCh(grantOwner); + m_p25->m_affiliations->touchGrant(grantOwner); } } else { @@ -2490,6 +2652,10 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ if (chNo > 0U) { ::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo); + if (!grp) { + m_p25->m_affiliations->setU2UGrantDirection(srcId, dstId); + } + if (grp) { // callback RPC to permit the granted TG on the specified voice channel if (m_p25->m_authoritative && m_p25->m_supervisor) { @@ -2601,6 +2767,9 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ chNo != m_p25->m_siteData.channelNo()) { json::object req = json::object(); req["dstId"].set(dstId); + req["srcId"].set(srcId); + bool unitToUnit = true; + req["unitToUnit"].set(unitToUnit); if (telInt) { req["telInt"].set(telInt); } @@ -2822,15 +2991,21 @@ void ControlSignaling::writeRF_TSDU_Grant_Update() m_mbfGrpGrntCnt++; break; } else { - uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId); + uint32_t srcId = m_p25->m_affiliations->getU2UGrantSource(dstId); + uint32_t targetId = m_p25->m_affiliations->getU2UGrantTarget(dstId); + if (srcId == 0U || targetId == 0U) { + noData = true; + m_mbfGrpGrntCnt++; + break; + } osp = std::make_unique(); DEBUG_LOG_TSBK(osp->toString()); - // transmit group voice grant update + // transmit unit-to-unit voice grant update osp->setLCO(TSBKO::OSP_UU_VCH_GRANT_UPD); osp->setSrcId(srcId); - osp->setDstId(dstId); + osp->setDstId(targetId); osp->setGrpVchId(voiceChData.chId()); osp->setGrpVchNo(chNo); @@ -3412,3 +3587,33 @@ void ControlSignaling::denialInhibit(uint32_t srcId) writeRF_TSDU_Ext_Func(ExtendedFunctions::INHIBIT, WUID_FNE, srcId); } } + +/* Produces the ordered key for a pending U2U request. */ + +uint64_t ControlSignaling::u2uTransactionKey(uint32_t callerId, uint32_t calledId) +{ + return (static_cast(callerId & 0xFFFFFFU) << 24) | (calledId & 0xFFFFFFU); +} + +/* Starts or replaces a pending U2U availability transaction. */ + +void ControlSignaling::startU2UAvailabilityTransaction(uint32_t callerId, uint32_t calledId, uint8_t serviceOptions, + bool originatingSite, bool answerRequired) +{ + // remove any existing transactions involving either the caller or called ID before starting a new one + for (auto it = m_u2uAvailabilityTransactions.begin(); it != m_u2uAvailabilityTransactions.end(); ) { + const U2UAvailabilityTransaction& transaction = it->second; + if (transaction.callerId == callerId || transaction.callerId == calledId || + transaction.calledId == callerId || transaction.calledId == calledId) { + it = m_u2uAvailabilityTransactions.erase(it); + } + else { + ++it; + } + } + + // generate the key for the new transaction + const uint64_t key = u2uTransactionKey(callerId, calledId); + m_u2uAvailabilityTransactions.emplace(key, U2UAvailabilityTransaction(callerId, calledId, serviceOptions, + originatingSite, answerRequired, U2U_AVAILABILITY_TIMER_TIMEOUT)); +} diff --git a/src/host/p25/packet/ControlSignaling.h b/src/host/p25/packet/ControlSignaling.h index 579a50d6..43370277 100644 --- a/src/host/p25/packet/ControlSignaling.h +++ b/src/host/p25/packet/ControlSignaling.h @@ -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) 2017-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL * */ /** @@ -161,9 +161,54 @@ namespace p25 friend class packet::Voice; friend class packet::Data; friend class p25::Control; - Control* m_p25; friend class lookups::P25AffiliationLookup; + /** + * @brief State of a pending unit-to-unit availability transaction. + */ + enum class U2UAvailabilityState : uint8_t + { + AWAITING, //!< Waiting for the called unit's initial response. + WAITING //!< Called unit responded WAIT; a later final response is allowed. + }; + + /** + * @brief Pending unit-to-unit availability-check transaction. + * @ingroup host_p25 + */ + struct U2UAvailabilityTransaction { + uint32_t callerId; //!< Original calling-unit RID. + uint32_t calledId; //!< Original called-unit RID. + uint8_t serviceOptions; //!< Service Options from the original UU VCH request. + U2UAvailabilityState state; //!< Current answer-processing state. + bool originatingSite; //!< True when this site received the original UU VCH request. + bool answerRequired; //!< True when the FNE explicitly transmitted a UU ANS REQ. + Timer responseTimer; //!< Response lifetime timer. + + /** + * @brief Initializes a pending availability-check transaction. + * @param caller Original calling-unit RID. + * @param called Original called-unit RID. + * @param options Service Options from the request. + * @param originating Flag indicating this site owns the originating transaction. + * @param required Flag indicating an answer response is required to complete call setup. + * @param timeoutSeconds Response lifetime in seconds. + */ + U2UAvailabilityTransaction(uint32_t caller, uint32_t called, uint8_t options, bool originating, bool required, uint32_t timeoutSeconds) : + callerId(caller), + calledId(called), + serviceOptions(options), + state(U2UAvailabilityState::AWAITING), + originatingSite(originating), + answerRequired(required), + responseTimer(1000U, timeoutSeconds) + { + responseTimer.start(); + } + }; + + Control* m_p25; + uint32_t m_patchSuperGroup; uint32_t m_announcementGroup; @@ -195,6 +240,7 @@ namespace p25 bool m_unitToUnitAvailCheck; bool m_telephoneInterconnect; std::unordered_set m_telIntGrants; + std::unordered_map m_u2uAvailabilityTransactions; uint8_t m_convFallbackPacketDelay; bool m_convFallback; @@ -232,6 +278,17 @@ namespace p25 */ virtual ~ControlSignaling(); + /** + * @brief Updates and expires pending unit-to-unit availability transactions. + * @param ms Elapsed milliseconds. + */ + void clock(uint32_t ms); + + /** + * @brief Clears all pending unit-to-unit availability transactions. + */ + void resetU2UAvailabilityTransactions(); + /** * @brief Write data processed from RF to the network. * @param tsbk TSBK to write to network. @@ -329,10 +386,11 @@ namespace p25 * @param skip Flag indicating normal grant checking is skipped. * @param chNo Channel Number. * @param telInt Flag indicating this grant is for a telephone interconnect call to the unit in dstId. + * @param u2uConfirmedUnitId Participant whose presence was confirmed by a correlated U2U answer, or zero. * @returns True, if granted, otherwise false. */ bool writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp, bool net = false, - bool skip = false, uint32_t chNo = 0U, bool telInt = false); + bool skip = false, uint32_t chNo = 0U, bool telInt = false, uint32_t u2uConfirmedUnitId = 0U); /** * @brief Helper to write a grant update packet. */ @@ -430,6 +488,24 @@ namespace p25 * @param srcId Source Radio ID. */ void denialInhibit(uint32_t srcId); + + /** + * @brief Produces the ordered key for a pending U2U request. + * @param callerId Original calling-unit RID. + * @param calledId Original called-unit RID. + * @returns uint64_t Ordered 48-bit request key. + */ + static uint64_t u2uTransactionKey(uint32_t callerId, uint32_t calledId); + + /** + * @brief Starts or replaces a pending U2U availability transaction. + * @param callerId Original calling-unit RID. + * @param calledId Original called-unit RID. + * @param serviceOptions Requested Service Options. + * @param originatingSite Flag indicating this site received the original request. + */ + void startU2UAvailabilityTransaction(uint32_t callerId, uint32_t calledId, uint8_t serviceOptions, + bool originatingSite, bool answerRequired); }; } // namespace packet } // namespace p25 diff --git a/src/host/p25/packet/Voice.cpp b/src/host/p25/packet/Voice.cpp index 4eb940e6..dfb3800b 100644 --- a/src/host/p25/packet/Voice.cpp +++ b/src/host/p25/packet/Voice.cpp @@ -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" @@ -164,7 +164,7 @@ bool Voice::process(uint8_t* data, uint32_t len) } // don't process RF frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != lc.getDstId()) { + if (!m_p25->isPermitted(lc.getSrcId(), lc.getDstId(), lc.getGroup())) { if (!g_disableNonAuthoritativeLogging) LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); resetRF(); @@ -262,7 +262,7 @@ bool Voice::process(uint8_t* data, uint32_t len) alreadyDecoded = true; - if (checkRFTrafficCollision(srcId, dstId)) + if (checkRFTrafficCollision(srcId, dstId, group)) return false; // if this is a late entry call, clear states @@ -426,7 +426,8 @@ bool Voice::process(uint8_t* data, uint32_t len) if (m_p25->m_enableControl) { // if the group wasn't granted out -- explicitly grant the group - if (!m_p25->m_affiliations->isGranted(dstId)) { + const bool callGranted = (group) ? m_p25->m_affiliations->isGranted(dstId) : m_p25->m_affiliations->isU2UGranted(srcId, dstId); + if (!callGranted) { if (m_p25->m_legacyGroupGrnt) { // are we auto-registering legacy radios to groups? if (m_p25->m_legacyGroupReg && group) { @@ -459,15 +460,17 @@ bool Voice::process(uint8_t* data, uint32_t len) // conventional registration or DVRS support? if ((m_p25->m_enableControl && !m_p25->m_dedicatedControl) || m_p25->m_voiceOnControl) { - if (!m_p25->m_affiliations->isGranted(dstId)) { + const bool callGranted = (group) ? m_p25->m_affiliations->isGranted(dstId) : m_p25->m_affiliations->isU2UGranted(srcId, dstId); + if (!callGranted) { m_p25->m_control->writeRF_TSDU_Grant(srcId, dstId, serviceOptions, group, false, true); } // if voice on control; insert grant updates before voice traffic if (m_p25->m_voiceOnControl) { - uint32_t chNo = m_p25->m_affiliations->getGrantedCh(dstId); + const uint32_t grantOwner = (group) ? dstId : m_p25->m_affiliations->getU2UGrantOwner(srcId, dstId); + uint32_t chNo = m_p25->m_affiliations->getGrantedCh(grantOwner); ::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo); - bool grp = m_p25->m_affiliations->isGroup(dstId); + bool grp = m_p25->m_affiliations->isGroup(grantOwner); std::unique_ptr osp; @@ -481,14 +484,14 @@ bool Voice::process(uint8_t* data, uint32_t len) osp->setGrpVchNo(chNo); } else { - uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId); + uint32_t grantedSrcId = m_p25->m_affiliations->getGrantedSrcId(grantOwner); osp = std::make_unique(); // transmit group voice grant update osp->setLCO(TSBKO::OSP_UU_VCH_GRANT_UPD); - osp->setSrcId(srcId); - osp->setDstId(dstId); + osp->setSrcId(grantedSrcId); + osp->setDstId(grantOwner); osp->setGrpVchId(voiceChData.chId()); osp->setGrpVchNo(chNo); } @@ -568,9 +571,10 @@ bool Voice::process(uint8_t* data, uint32_t len) // if voice on control; insert group voice channel updates directly after HDU but before LDUs if (m_p25->m_voiceOnControl) { - uint32_t chNo = m_p25->m_affiliations->getGrantedCh(dstId); + const uint32_t grantOwner = (group) ? dstId : m_p25->m_affiliations->getU2UGrantOwner(srcId, dstId); + uint32_t chNo = m_p25->m_affiliations->getGrantedCh(grantOwner); ::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo); - bool grp = m_p25->m_affiliations->isGroup(dstId); + bool grp = m_p25->m_affiliations->isGroup(grantOwner); std::unique_ptr osp; @@ -584,14 +588,14 @@ bool Voice::process(uint8_t* data, uint32_t len) osp->setGrpVchNo(chNo); } else { - uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId); + uint32_t grantedSrcId = m_p25->m_affiliations->getGrantedSrcId(grantOwner); osp = std::make_unique(); // transmit group voice grant update osp->setLCO(TSBKO::OSP_UU_VCH_GRANT_UPD); - osp->setSrcId(srcId); - osp->setDstId(dstId); + osp->setSrcId(grantedSrcId); + osp->setDstId(grantOwner); osp->setGrpVchId(voiceChData.chId()); osp->setGrpVchNo(chNo); } @@ -621,7 +625,7 @@ bool Voice::process(uint8_t* data, uint32_t len) if (m_p25->m_rfState == RS_RF_AUDIO) { // don't process RF frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != m_rfLC.getDstId()) { + if (!m_p25->isPermitted(m_rfLC.getSrcId(), m_rfLC.getDstId(), m_rfLC.getGroup())) { if (!g_disableNonAuthoritativeLogging) LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); resetRF(); @@ -1236,7 +1240,7 @@ bool Voice::processNetwork(uint8_t* data, uint32_t len, lc::LC& control, data::L uint32_t dstId = control.getDstId(); uint32_t srcId = control.getSrcId(); - if (checkNetTrafficCollision(srcId, dstId, duid)) + if (checkNetTrafficCollision(srcId, dstId, control.getGroup(), duid)) return false; if (m_p25->m_netState == RS_NET_AUDIO && (duid != DUID::TDU && duid != DUID::TDULC)) @@ -1521,10 +1525,10 @@ void Voice::writeNetwork(const uint8_t *data, defines::DUID::E duid, defines::Fr /* Helper to perform RF traffic collision checking. */ -bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId) +bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId, bool group) { // don't process RF frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != dstId) { + if (!m_p25->isPermitted(srcId, dstId, group)) { if (!g_disableNonAuthoritativeLogging) LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); resetRF(); @@ -1585,7 +1589,7 @@ bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId) /* Helper to perform network traffic collision checking. */ -bool Voice::checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, defines::DUID::E duid) +bool Voice::checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, bool group, defines::DUID::E duid) { // safety check: if the network is marked as not idle, but the last network destination ID is 0 *and* the // network watchdog isn't running, then reset the network state to idle @@ -1690,7 +1694,7 @@ bool Voice::checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, defines::DU } // don't process network frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != dstId) { + if (!m_p25->isPermitted(srcId, dstId, group)) { if (!g_disableNonAuthoritativeLogging) LogWarning(LOG_NET, "[NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted, dstId = %u", dstId); resetNet(); @@ -1912,7 +1916,8 @@ void Voice::writeNet_LDU1() (m_netLC.getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag (m_netLC.getPriority() & 0x07U); // Priority - if (!m_p25->m_affiliations->isGranted(dstId)) { + const bool callGranted = (group) ? m_p25->m_affiliations->isGranted(dstId) : m_p25->m_affiliations->isU2UGranted(srcId, dstId); + if (!callGranted) { if (!m_p25->m_control->writeRF_TSDU_Grant(srcId, dstId, serviceOptions, group, true)) { if (!m_p25->m_hideRemoteGrantLog) { LogError(LOG_NET, P25_HDU_STR " call rejected, network call not granted, dstId = %u", dstId); @@ -1953,9 +1958,10 @@ void Voice::writeNet_LDU1() // if voice on control; insert grant updates before voice traffic if (m_p25->m_voiceOnControl) { - uint32_t chNo = m_p25->m_affiliations->getGrantedCh(dstId); + const uint32_t grantOwner = (group) ? dstId : m_p25->m_affiliations->getU2UGrantOwner(srcId, dstId); + uint32_t chNo = m_p25->m_affiliations->getGrantedCh(grantOwner); ::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo); - bool grp = m_p25->m_affiliations->isGroup(dstId); + bool grp = m_p25->m_affiliations->isGroup(grantOwner); std::unique_ptr osp; @@ -1969,14 +1975,14 @@ void Voice::writeNet_LDU1() osp->setGrpVchNo(chNo); } else { - uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId); + uint32_t grantedSrcId = m_p25->m_affiliations->getGrantedSrcId(grantOwner); osp = std::make_unique(); - // transmit group voice grant update + // transmit unit-to-unit voice grant update osp->setLCO(TSBKO::OSP_UU_VCH_GRANT_UPD); - osp->setSrcId(srcId); - osp->setDstId(dstId); + osp->setSrcId(grantedSrcId); + osp->setDstId(grantOwner); osp->setGrpVchId(voiceChData.chId()); osp->setGrpVchNo(chNo); } @@ -2197,7 +2203,7 @@ void Voice::writeNet_LDU2() uint32_t dstId = control.getDstId(); // don't process network frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != dstId) { + if (!m_p25->isPermitted(control.getSrcId(), dstId, control.getGroup())) { if (!g_disableNonAuthoritativeLogging) LogWarning(LOG_NET, "[NON-AUTHORITATIVE] Ignoring network traffic (LDU2), destination not permitted!"); resetNet(); diff --git a/src/host/p25/packet/Voice.h b/src/host/p25/packet/Voice.h index 8e1743e8..9e198f07 100644 --- a/src/host/p25/packet/Voice.h +++ b/src/host/p25/packet/Voice.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016,2017 Jonathan Naylor, G4KLX -* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL +* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL * */ /** @@ -169,17 +169,19 @@ namespace p25 * @brief Helper to perform RF traffic collision checking. * @param srcId Source ID. * @param dstId Destination ID. + * @param group Flag indicating group traffic. * @return bool True, if traffic collision, otherwise false. */ - bool checkRFTrafficCollision(uint32_t srcId, uint32_t dstId); + bool checkRFTrafficCollision(uint32_t srcId, uint32_t dstId, bool group); /** * @brief Helper to perform network traffic collision checking. * @param srcId Source ID. * @param dstId Destination ID. + * @param group Flag indicating group traffic. * @param duid DUID. * @return bool True, if traffic collision, otherwise false. */ - bool checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, defines::DUID::E duid); + bool checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, bool group, defines::DUID::E duid); /** * @brief Helper to write end of voice frame data. diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index 4cb6994f..ec40126b 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -18,7 +18,9 @@ #include "common/nxdn/channel/LICH.h" #include "common/nxdn/channel/SACCH.h" #include "common/nxdn/lc/RTCH.h" +#include "common/edac/Trellis.h" #include "common/p25/lc/tsbk/TSBKFactory.h" +#include "common/p25/P25Utils.h" #include "host/p25/packet/ControlSignaling.h" #include @@ -422,6 +424,59 @@ bool HostTestHooks::p25ProcessRawTSBK(p25::Control& control, const uint8_t* raw) return control.m_control->process(frame, sizeof(frame), std::move(tsbk)); } +/* Injects an independently encoded raw TSBK through P25 network processing. */ + +bool HostTestHooks::p25ProcessNetworkRawTSBK(p25::Control& control, const uint8_t* raw) +{ + uint8_t fec[p25::defines::P25_TSBK_FEC_LENGTH_BYTES] = { 0U }; + uint8_t encoded[p25::defines::P25_TSDU_FRAME_LENGTH_BYTES] = { 0U }; + edac::Trellis trellis; + trellis.encode12(raw, fec); + p25::P25Utils::encode(fec, encoded, 114U, 318U); + + p25::lc::LC lc; + p25::data::LowSpeedData lsd; + p25::defines::DUID::E duid = p25::defines::DUID::TSDU; + return control.m_control->processNetwork(encoded, sizeof(encoded), lc, lsd, duid); +} + +/* Tests whether one P25 private grant owns the specified RID pair. */ + +bool HostTestHooks::p25IsU2UGranted(const p25::Control& control, uint32_t srcId, uint32_t dstId) +{ + return control.m_affiliations->isU2UGranted(srcId, dstId); +} + +/* Gets the current source RID advertised for a P25 private grant. */ + +uint32_t HostTestHooks::p25U2UGrantSource(const p25::Control& control, uint32_t srcId, uint32_t dstId) +{ + const uint32_t owner = control.m_affiliations->getU2UGrantOwner(srcId, dstId); + return control.m_affiliations->getU2UGrantSource(owner); +} + +/* Gets the current target RID advertised for a P25 private grant. */ + +uint32_t HostTestHooks::p25U2UGrantTarget(const p25::Control& control, uint32_t srcId, uint32_t dstId) +{ + const uint32_t owner = control.m_affiliations->getU2UGrantOwner(srcId, dstId); + return control.m_affiliations->getU2UGrantTarget(owner); +} + +/* Gets the number of pending P25 U2U availability transactions. */ + +size_t HostTestHooks::p25PendingU2UCount(const p25::Control& control) +{ + return control.m_control->m_u2uAvailabilityTransactions.size(); +} + +/* Advances P25 U2U availability transaction timers. */ + +void HostTestHooks::p25ClockU2U(p25::Control& control, uint32_t ms) +{ + control.m_control->clock(ms); +} + /* Releases a P25 grant for a steering-policy test. */ bool HostTestHooks::p25ReleaseGrant(p25::Control& control, uint32_t dstId) @@ -429,6 +484,20 @@ bool HostTestHooks::p25ReleaseGrant(p25::Control& control, uint32_t dstId) return control.m_affiliations->releaseGrant(dstId, false); } +/* Releases the active P25 private grant involving a radio ID. */ + +bool HostTestHooks::p25ReleaseU2UGrant(p25::Control& control, uint32_t unitId) +{ + return control.m_affiliations->releaseU2UGrant(unitId); +} + +/* Applies the P25 voice-channel grant-release policy. */ + +void HostTestHooks::p25VoiceChannelReleaseGrant(p25::Control& control, uint32_t dstId) +{ + control.releaseGrantFromVoiceChannel(dstId); +} + /* Gets the last P25 RF destination ID. */ uint32_t HostTestHooks::p25RFLastDstId(const p25::Control& control) { return control.m_rfLastDstId; } diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index b6bb63d4..9f98d632 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -259,6 +259,49 @@ public: * @returns bool True if the TSBK was accepted; otherwise, false. */ static bool p25ProcessRawTSBK(p25::Control& control, const uint8_t* raw); + /** + * @brief Injects an independently encoded raw TSBK through P25 network processing. + * @param control P25 control instance. + * @param raw Raw 12-byte TSBK including CRC. + * @returns bool True if the network TSBK was accepted; otherwise, false. + */ + static bool p25ProcessNetworkRawTSBK(p25::Control& control, const uint8_t* raw); + /** + * @brief Tests whether one P25 private grant owns the specified RID pair. + * @param control P25 control instance. + * @param srcId One participant RID. + * @param dstId The other participant RID. + * @returns bool True if the pair resolves to an active private grant. + */ + static bool p25IsU2UGranted(const p25::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Gets the current source RID advertised for a P25 private grant. + * @param control P25 control instance. + * @param srcId One private-call participant. + * @param dstId The other private-call participant. + * @returns uint32_t Current advertised source RID, or zero. + */ + static uint32_t p25U2UGrantSource(const p25::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Gets the current target RID advertised for a P25 private grant. + * @param control P25 control instance. + * @param srcId One private-call participant. + * @param dstId The other private-call participant. + * @returns uint32_t Current advertised target RID, or zero. + */ + static uint32_t p25U2UGrantTarget(const p25::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Gets the number of pending P25 U2U availability transactions. + * @param control P25 control instance. + * @returns size_t Number of pending transactions. + */ + static size_t p25PendingU2UCount(const p25::Control& control); + /** + * @brief Advances P25 U2U availability transaction timers. + * @param control P25 control instance. + * @param ms Elapsed milliseconds. + */ + static void p25ClockU2U(p25::Control& control, uint32_t ms); /** * @brief Releases a P25 grant for a steering-policy test. * @param control P25 control instance. @@ -266,6 +309,19 @@ public: * @returns bool True if the grant was released; otherwise, false. */ static bool p25ReleaseGrant(p25::Control& control, uint32_t dstId); + /** + * @brief Releases the active P25 private grant involving a radio ID. + * @param control P25 control instance. + * @param unitId RID of either private-call participant. + * @returns bool True if a private grant was released. + */ + static bool p25ReleaseU2UGrant(p25::Control& control, uint32_t unitId); + /** + * @brief Applies the P25 voice-channel grant-release policy. + * @param control P25 control instance. + * @param dstId Destination-keyed grant owner reported by the voice channel. + */ + static void p25VoiceChannelReleaseGrant(p25::Control& control, uint32_t dstId); /** * @brief Gets the last P25 RF destination ID. * @param control P25 control instance. diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 48bfadc0..3faf0307 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -179,6 +179,34 @@ void buildUUAnswerResponse(uint32_t responderId, uint32_t callerId, uint8_t resp edac::CRC::addCCITT162(raw, p25::defines::P25_TSBK_LENGTH_BYTES); } +/** + * @brief Builds an independently encoded cancel service request ISP TSBK. + * @param srcId RID cancelling the service. + * @param serviceType Service opcode being cancelled. + * @param raw The buffer to store the constructed TSBK. + */ +void buildCancelServiceRequest(uint32_t srcId, uint8_t serviceType, uint8_t* raw) +{ + ::memset(raw, 0x00U, p25::defines::P25_TSBK_LENGTH_BYTES); + raw[0U] = 0x80U | p25::defines::TSBKO::ISP_CAN_SRV_REQ; + raw[1U] = p25::defines::MFG_STANDARD; + raw[2U] = serviceType & 0x3FU; + SET_UINT24(srcId, raw, 7U); + edac::CRC::addCCITT162(raw, p25::defines::P25_TSBK_LENGTH_BYTES); +} + +/** Builds an independently encoded UU ANS request OSP TSBK. */ +void buildUUAnswerRequest(uint32_t callerId, uint32_t calledId, uint8_t* raw) +{ + ::memset(raw, 0x00U, p25::defines::P25_TSBK_LENGTH_BYTES); + raw[0U] = 0x80U | p25::defines::TSBKO::IOSP_UU_ANS; + raw[1U] = p25::defines::MFG_STANDARD; + raw[2U] = 0x04U; + SET_UINT24(calledId, raw, 4U); + SET_UINT24(callerId, raw, 7U); + edac::CRC::addCCITT162(raw, p25::defines::P25_TSBK_LENGTH_BYTES); +} + } // namespace // --------------------------------------------------------------------------- @@ -644,7 +672,129 @@ TEST_CASE("P25 unit-to-unit grant admits either participant as first talker", // availability-check procedure. Voice admission therefore must recognize // the same private call when its direction is reversed. CHECK(HostTestHooks::p25IsGranted(*harness.m_control, calledUnit)); - CHECK(HostTestHooks::p25IsGranted(*harness.m_control, callingUnit)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, callingUnit)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, callingUnit, calledUnit)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, calledUnit, callingUnit)); + + // A VC terminator ends one talkspurt, not the message-trunked private call. + HostTestHooks::p25VoiceChannelReleaseGrant(*harness.m_control, callingUnit); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, callingUnit, calledUnit)); + + REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, callingUnit)); + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, callingUnit, calledUnit)); +} + +TEST_CASE("P25 non-authoritative unit-to-unit permit admits either participant direction", + "[p25][host][control][u2u][permit]") +{ + P25HostHarness harness(false); + const uint32_t caller = 5519007U; + const uint32_t called = 5519001U; + + harness.m_control->permittedTG(called, false, false, caller, true); + + CHECK(harness.m_control->isPermitted(caller, called, false)); + CHECK(harness.m_control->isPermitted(called, caller, false)); + CHECK_FALSE(harness.m_control->isPermitted(caller, 5519002U, false)); + CHECK_FALSE(harness.m_control->isPermitted(caller, called, true)); + + harness.m_control->permittedTG(0U); + CHECK_FALSE(harness.m_control->isPermitted(caller, called, false)); +} + +TEST_CASE("P25 voice-channel release still releases group grants immediately", + "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t srcId = 1001U; + const uint32_t dstId = 2001U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + REQUIRE(HostTestHooks::p25SeedGrant(*harness.m_control, srcId, dstId, false)); + + HostTestHooks::p25VoiceChannelReleaseGrant(*harness.m_control, dstId); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, dstId)); +} + +TEST_CASE("P25 active unit-to-unit call continues with a grant instead of another answer request", + "[p25][host][control][u2u][procedure][continuation]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519007U; + const uint32_t called = 5519001U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + + // The post-answer grant is answering unit -> original caller. + REQUIRE(HostTestHooks::p25SeedU2UGrant(*harness.m_control, called, caller)); + HostTestHooks::p25VoiceChannelReleaseGrant(*harness.m_control, caller); + REQUIRE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); + REQUIRE(HostTestHooks::p25U2UGrantSource(*harness.m_control, caller, called) == called); + REQUIRE(HostTestHooks::p25U2UGrantTarget(*harness.m_control, caller, called) == caller); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); + CHECK(HostTestHooks::p25U2UGrantSource(*harness.m_control, caller, called) == caller); + CHECK(HostTestHooks::p25U2UGrantTarget(*harness.m_control, caller, called) == called); +} + +TEST_CASE("P25 unit-to-unit cancellation cleanup releases only private call state", + "[p25][host][control][u2u][procedure][cancel]") +{ + P25HostHarness harness; + const uint32_t privateChannelNo = 1U; + const uint32_t groupChannelNo = 2U; + const uint32_t caller = 5519007U; + const uint32_t called = 5519001U; + const uint32_t groupSrcId = 1001U; + const uint32_t groupDstId = 2001U; + harness.m_chLookup.setRFChData(privateChannelNo, lookups::VoiceChData(1U, privateChannelNo, "", 0U, "")); + harness.m_chLookup.setRFChData(groupChannelNo, lookups::VoiceChData(1U, groupChannelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(privateChannelNo)); + REQUIRE(harness.m_chLookup.initializeRFCh(groupChannelNo)); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + REQUIRE(HostTestHooks::p25SeedU2UGrant(*harness.m_control, caller, called)); + REQUIRE(HostTestHooks::p25SeedGrant(*harness.m_control, groupSrcId, groupDstId, false)); + + REQUIRE(HostTestHooks::p25ReleaseU2UGrant(*harness.m_control, caller)); + + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); + CHECK(HostTestHooks::p25U2UGrantSource(*harness.m_control, caller, called) == 0U); + CHECK(HostTestHooks::p25U2UGrantTarget(*harness.m_control, caller, called) == 0U); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, groupDstId)); +} + +TEST_CASE("P25 cancel service request clears pending unit-to-unit setup", + "[p25][host][control][u2u][procedure][cancel]") +{ + P25HostHarness harness; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + REQUIRE(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); + + buildCancelServiceRequest(called, p25::defines::TSBKO::IOSP_UU_ANS, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); } TEST_CASE("P25 unit-to-unit availability-check procedure grants only after proceed", @@ -660,10 +810,10 @@ TEST_CASE("P25 unit-to-unit availability-check procedure grants only after proce REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); HostTestHooks::p25ConfigureU2U(*harness.m_control, true); HostTestHooks::p25RegisterUnit(*harness.m_control, caller); - HostTestHooks::p25RegisterUnit(*harness.m_control, called); buildUUVchRequest(caller, called, raw); REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, caller)); CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, called)); @@ -675,6 +825,40 @@ TEST_CASE("P25 unit-to-unit availability-check procedure grants only after proce buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); CHECK(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, called)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, called, caller)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + + // A duplicate final response is no longer correlated and must not create + // another grant or pending transaction. + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); +} + +TEST_CASE("P25 unit-to-unit deny consumes the availability transaction without a grant", + "[p25][host][control][u2u][procedure]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + HostTestHooks::p25RegisterUnit(*harness.m_control, called); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + REQUIRE(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_DENY, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); } TEST_CASE("P25 unit-to-unit rejects unsolicited and mismatched answer responses", @@ -697,7 +881,6 @@ TEST_CASE("P25 unit-to-unit rejects unsolicited and mismatched answer responses" buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, caller)); - REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, caller)); buildUUVchRequest(caller, called, raw); REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); @@ -706,6 +889,33 @@ TEST_CASE("P25 unit-to-unit rejects unsolicited and mismatched answer responses" CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, unrelatedCaller)); } +TEST_CASE("P25 unit-to-unit availability transaction expires before a late response", + "[p25][host][control][u2u][procedure]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + HostTestHooks::p25RegisterUnit(*harness.m_control, called); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + REQUIRE(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); + + HostTestHooks::p25ClockU2U(*harness.m_control, 16000U); + REQUIRE(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); +} + TEST_CASE("P25 unit-to-unit without availability check grants the request immediately", "[p25][host][control][u2u][procedure]") { @@ -723,6 +933,71 @@ TEST_CASE("P25 unit-to-unit without availability check grants the request immedi buildUUVchRequest(caller, called, raw); REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); CHECK(HostTestHooks::p25IsGranted(*harness.m_control, called)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); + + // A manual-answer SU may respond after the unchecked grant has expired. + // The response is correlated to the original request and must re-establish + // the same private call without accepting an unsolicited answer response. + REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, called)); + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); +} + +TEST_CASE("P25 unit-to-unit network answer completes the originating-site transaction", + "[p25][host][control][u2u][procedure][network]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + HostTestHooks::p25RegisterUnit(*harness.m_control, called); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + REQUIRE(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessNetworkRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, called)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); +} + +TEST_CASE("P25 unit-to-unit target site correlates a network answer request with the RF response", + "[p25][host][control][u2u][procedure][network]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, called); + + buildUUAnswerRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessNetworkRawTSBK(*harness.m_control, raw)); + REQUIRE(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 1U); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, called)); + CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); } TEST_CASE("P25 host preserves encrypted network call parameters", "[p25][host][control][crypto]")