correct P25 unit-to-unit call setup and message-trunked continuation; correlate UU answer responses with pending availability checks; track private grants and voice channel permits by subscriber pair; preserve grants across talkspurts, advertise the current requester as first-to-talk, and clean up U2U state on cancellation; add comprehensive U2U regression tests;

pull/134/head
Bryan Biedenkapp 4 days ago
parent b555b2a4f0
commit bcb8894154

@ -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<bool>();
}
uint32_t srcId = 0U;
if (req["srcId"].is<int>()) {
srcId = req["srcId"].get<uint32_t>();
}
bool unitToUnit = false;
if (req["unitToUnit"].is<bool>()) {
unitToUnit = req["unitToUnit"].get<bool>();
}
// 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. */

@ -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.

@ -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 <algorithm>
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<uint64_t>(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<uint32_t>((entry.first >> 24) & 0xFFFFFFU);
const uint32_t high = static_cast<uint32_t>(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<uint32_t> P25AffiliationLookup::clearGroupAff(uint32_t dstId, bool releaseAll)
@ -48,8 +94,39 @@ std::vector<uint32_t> 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<uint32_t>((entry.first >> 24) & 0xFFFFFFU);
const uint32_t high = static_cast<uint32_t>(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<uint32_t>((entry.first >> 24) & 0xFFFFFFU);
const uint32_t high = static_cast<uint32_t>(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;
}

@ -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 <unordered_map>
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<uint64_t, uint32_t> m_u2uGrantOwners; //!< Maps an unordered private-call RID pair to one destination-key grant owner.
std::unordered_map<uint32_t, uint32_t> m_u2uGrantSources; //!< Maps a private grant owner to the current first-to-talk/source RID.
std::unordered_map<uint32_t, uint32_t> 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

@ -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<lc::TSBK> preDecodedTSBK)
@ -257,15 +259,39 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr<lc::
VERIFY_SRCID_REG(tsbk->toString(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_ptr<lc::
tsbk->toString(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_ptr<lc::
}
}
// a U2U cancellation ends both call setup and an established
// message-trunked call involving the requesting subscriber
if (service == TSBKO::IOSP_UU_VCH || service == TSBKO::IOSP_UU_ANS) {
for (auto it = m_u2uAvailabilityTransactions.begin(); it != m_u2uAvailabilityTransactions.end(); ) {
if (it->second.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<uint32_t>(dstId);
req["srcId"].set<uint32_t>(srcId);
bool unitToUnit = true;
req["unitToUnit"].set<bool>(unitToUnit);
if (telInt) {
req["telInt"].set<bool>(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<OSP_UU_VCH_GRANT_UPD>();
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<uint64_t>(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));
}

@ -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<uint32_t> m_telIntGrants;
std::unordered_map<uint64_t, U2UAvailabilityTransaction> 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

@ -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<lc::TSBK> 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<lc::tsbk::OSP_UU_VCH_GRANT_UPD>();
// 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<lc::TSBK> 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<lc::tsbk::OSP_UU_VCH_GRANT_UPD>();
// 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<lc::TSBK> 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<lc::tsbk::OSP_UU_VCH_GRANT_UPD>();
// 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();

@ -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.

@ -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 <cstring>
@ -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; }

@ -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.

@ -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]")

Loading…
Cancel
Save

Powered by TurnKey Linux.