correct DMR and NXDN private-call setup and network routing; implement DMR Tier III OACSU availability checks with response correlation, collision handling, retries, cancellation, and timeout cleanup; validate conventional private-call destinations without disturbing group routing; correct NXDN individual-call request, response, assignment, duplicate assignment, disconnect, and transmission-release signaling; fix NXDN response-cause decoding and ten-bit traffic-channel encoding; route private calls to registered trunked or eligible conventional peers; add DMR, NXDN, host, codec, and FNE regression tests;

pull/134/head
Bryan Biedenkapp 4 days ago
parent 2ba4dbf97c
commit 1b1d508d6f

@ -194,6 +194,8 @@ protocols:
slot: 1
# Flag indicating whether or not the source ID validation before granting disabled.
disableGrantSourceIdCheck: false
# Check that the called subscriber is available before assigning an individual voice call.
unitToUnitAvailCheck: false
# Require ETSI DMR authentication before accepting unit registration. The 16-byte
# authentication key is requested from the upstream FNE through the existing LLA key path.
requireAuthForReg: false

@ -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) 2022,2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022,2024,2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -385,8 +385,11 @@ namespace nxdn
RTCH_SDCALL_RESP = 0x3BU, //!< SDCALL_RESP - Short Data Call Response
// Control Channel Message Types
RCCH_VCALL_REQ = 0x01U, //!< VCALL_REQ (ISP) / VCALL_RESP (OSP)
RCCH_VCALL_REC = 0x02U, //!< VCALL_REC_RESP (ISP) / VCALL_REC_REQ (OSP)
RCCH_VCALL_CONN = 0x03U, //!< VCALL_CONN - Voice Call Connection Request (ISP) / Voice Call Connection Response (OSP)
RCCH_VCALL_ASSGN = 0x04U, //!< VCALL_ASSGN - Voice Call Assignment
RCCH_VCALL_ASSGN_DUP = 0x05U, //!< VCALL_ASSGN_DUP - Voice Call Assignment Duplicate
RCCH_DCALL_ASSGN = 0x14U, //!< DCALL_ASSGN - Data Call Assignment
RCCH_SITE_INFO = 0x18U, //!< SITE_INFO - Site Information
RCCH_REG = 0x20U, //!< REG - Registration Request (ISP) / Registration Response (OSP)

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2018 Jonathan Naylor, G4KLX
* Copyright (C) 2022,2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022,2024,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "nxdn/NXDNDefines.h"
@ -43,6 +43,7 @@ RTCH::RTCH() :
m_group(true),
m_duplex(false),
m_transmissionMode(TransmissionMode::MODE_4800),
m_transmissionPermit(false),
m_packetInfo(),
m_rsp(),
m_dataFrameNumber(0U),
@ -70,6 +71,7 @@ RTCH::RTCH(const RTCH& data) :
m_group(true),
m_duplex(false),
m_transmissionMode(TransmissionMode::MODE_4800),
m_transmissionPermit(false),
m_packetInfo(),
m_rsp(),
m_dataFrameNumber(0U),
@ -237,6 +239,8 @@ bool RTCH::decodeLC(const uint8_t* data)
m_priority = (data[1U] & 0x20U) == 0x20U; // Priority Flag
m_srcId = (uint16_t)((data[3U] << 8) | data[4U]) & 0xFFFFU; // Source Radio Address
m_dstId = (uint16_t)((data[5U] << 8) | data[6U]) & 0xFFFFU; // Target Radio Address
m_transmissionPermit = m_messageType == MessageType::RTCH_TX_REL_EX &&
(data[7U] & 0x80U) == 0x80U; // Transmission Permit
break;
case MessageType::RTCH_DCALL_HDR:
m_callType = (data[2U] >> 5) & 0x07U; // Call Type
@ -362,6 +366,8 @@ void RTCH::encodeLC(uint8_t* data)
data[4U] = (m_srcId >> 0U) & 0xFFU; // ...
data[5U] = (m_dstId >> 8U) & 0xFFU; // Target Radio Address
data[6U] = (m_dstId >> 0U) & 0xFFU; // ...
if (m_messageType == MessageType::RTCH_TX_REL_EX)
data[7U] = m_transmissionPermit ? 0x80U : 0x00U; // Transmission Permit
break;
case MessageType::RTCH_DCALL_HDR:
data[1U] = (m_emergency ? 0x80U : 0x00U) + // Emergency Flag
@ -465,6 +471,7 @@ void RTCH::copy(const RTCH& data)
m_group = data.m_group;
m_duplex = data.m_duplex;
m_transmissionMode = data.m_transmissionMode;
m_transmissionPermit = data.m_transmissionPermit;
m_packetInfo = data.m_packetInfo;
m_rsp = data.m_rsp;

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2018 Jonathan Naylor, G4KLX
* Copyright (C) 2022 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022,2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -147,6 +147,10 @@ namespace nxdn
* @brief Transmission mode.
*/
DECLARE_PROPERTY(uint8_t, transmissionMode, TransmissionMode);
/**
* @brief Transmission right flag carried by TX_REL_EX.
*/
DECLARE_PROPERTY(bool, transmissionPermit, TransmissionPermit);
/** @} */
/** @name Data Call Data */

@ -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
*
*/
#include "Defines.h"
@ -38,6 +38,16 @@ void MESSAGE_TYPE_VCALL_ASSGN::decode(const uint8_t* data, uint32_t length, uint
::memset(rcch, 0x00U, NXDN_RCCH_LC_LENGTH_BYTES + 4U);
RCCH::decode(data, rcch, length, offset);
m_callType = (rcch[2U] >> 5) & 0x07U; // Call Type
m_group = m_callType != CallType::INDIVIDUAL; // Group Flag
m_emergency = (rcch[1U] & 0x80U) == 0x80U; // Emergency Flag
m_priority = (rcch[1U] & 0x20U) == 0x20U; // Priority Flag
m_duplex = (rcch[2U] & 0x10U) == 0x10U; // Half/Full Duplex Flag
m_transmissionMode = rcch[2U] & 0x07U; // Transmission Mode
m_srcId = (uint16_t)((rcch[3U] << 8) | rcch[4U]) & 0xFFFFU; // Source Radio Address
m_dstId = (uint16_t)((rcch[5U] << 8) | rcch[6U]) & 0xFFFFU; // Target Radio Address
m_grpVchNo = (uint16_t)(((rcch[7U] & 0x03U) << 8) | rcch[8U]); // Channel
}
/* Encode RCCH data. */
@ -60,7 +70,7 @@ void MESSAGE_TYPE_VCALL_ASSGN::encode(uint8_t* data, uint32_t length, uint32_t o
rcch[5U] = (m_dstId >> 8U) & 0xFFU; // Target Radio Address
rcch[6U] = (m_dstId >> 0U) & 0xFFU; // ...
rcch[7U] = (m_grpVchNo >> 10) & 0x03U; // Channel
rcch[7U] = (m_grpVchNo >> 8) & 0x03U; // Channel
rcch[8U] = (m_grpVchNo & 0xFFU); // ...
rcch[10U] = (s_siteData.locId() >> 8) & 0xFFU; // Location ID

@ -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
*
*/
#include "Defines.h"
@ -40,12 +40,14 @@ void MESSAGE_TYPE_VCALL_CONN::decode(const uint8_t* data, uint32_t length, uint3
RCCH::decode(data, rcch, length, offset);
m_callType = (rcch[2U] >> 5) & 0x07U; // Call Type
m_group = m_callType != CallType::INDIVIDUAL; // Group Flag
m_emergency = (rcch[1U] & 0x80U) == 0x80U; // Emergency Flag
m_priority = (rcch[1U] & 0x20U) == 0x20U; // Priority Flag
m_duplex = (rcch[2U] & 0x10U) == 0x10U; // Half/Full Duplex Flag
m_transmissionMode = (rcch[2U] & 0x07U); // Transmission Mode
m_srcId = (uint16_t)((rcch[3U] << 8) | rcch[4U]) & 0xFFFFU; // Source Radio Address
m_dstId = (uint16_t)((rcch[5U] << 8) | rcch[6U]) & 0xFFFFU; // Target Radio Address
m_causeRsp = rcch[7U]; // Cause (VD/DREQ/DISC)
}
/* Encode RCCH data. */

@ -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
*
*/
#include "Defines.h"
@ -42,8 +42,13 @@ std::unique_ptr<RCCH> RCCHFactory::createRCCH(const uint8_t* data, uint32_t leng
// message type opcodes
switch (messageType) {
case MessageType::RTCH_VCALL:
case MessageType::RCCH_VCALL_REC:
case MessageType::RCCH_VCALL_CONN:
case MessageType::DISC:
return decode(new MESSAGE_TYPE_VCALL_CONN(), data, length, offset);
case MessageType::RCCH_VCALL_ASSGN:
case MessageType::RCCH_VCALL_ASSGN_DUP:
return decode(new MESSAGE_TYPE_VCALL_ASSGN(), data, length, offset);
case MessageType::RTCH_DCALL_HDR:
return decode(new MESSAGE_TYPE_DCALL_HDR(), data, length, offset);
case MessageType::IDLE:

@ -499,7 +499,7 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId
m_statusPVCall[dstId].activeCall = true;
// find the SSRC of the peer that registered this unit
uint32_t regSSRC = m_network->findPeerUnitReg(srcId);
uint32_t regSSRC = m_network->findPeerUnitReg(dstId);
m_statusPVCall[dstId].dstPeerId = regSSRC;
m_statusPVCall.unlock();
@ -753,21 +753,33 @@ bool TagDMRData::processGrantReq(uint32_t srcId, uint32_t dstId, uint8_t slot, b
}
}
lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId);
if (!unitToUnit) {
lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId);
// check TGID validity
if (tg.isInvalid()) {
return false;
}
// check TGID validity
if (tg.isInvalid()) {
return false;
}
if (!tg.config().active()) {
return false;
if (!tg.config().active()) {
return false;
}
}
else {
lookups::RadioId target = m_network->m_ridLookup->find(dstId);
if ((!target.radioDefault() && !target.radioEnabled()) ||
(target.radioDefault() && m_network->m_rejectUnknownRID)) {
return false;
}
}
// repeat traffic to the connected peers
if (m_network->peerCount() > 0U) {
const uint32_t registeredPeerId = unitToUnit ? m_network->findPeerUnitReg(dstId) : 0U;
for (const auto& peer : m_network->peersSnapshot()) {
if (peerId != peer.first) {
if (registeredPeerId != 0U && peer.first != registeredPeerId)
continue;
write_CSBK_Grant(peer.first, srcId, dstId, 4U, !unitToUnit);
}
}
@ -1484,9 +1496,14 @@ bool TagDMRData::write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstI
return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior
}
else {
if (!aff->hasGroupAff(dstId)) {
if (grp && !aff->hasGroupAff(dstId)) {
return false;
}
if (!grp && !aff->isUnitReg(dstId)) {
TrafficNetwork::PeerConnectionPtr connection = m_network->findPeer(peerId);
if (connection == nullptr || !connection->isConventional())
return false;
}
}
if (grp) {

@ -516,7 +516,7 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI
m_statusPVCall[dstId].activeCall = true;
// find the SSRC of the peer that registered this unit
uint32_t regSSRC = m_network->findPeerUnitReg(srcId);
uint32_t regSSRC = m_network->findPeerUnitReg(dstId);
m_statusPVCall[dstId].dstPeerId = regSSRC;
m_statusPVCall.unlock();
@ -764,21 +764,33 @@ bool TagNXDNData::processGrantReq(uint32_t srcId, uint32_t dstId, bool unitToUni
}
}
lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId);
if (!unitToUnit) {
lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId);
// check TGID validity
if (tg.isInvalid()) {
return false;
}
// check TGID validity
if (tg.isInvalid()) {
return false;
}
if (!tg.config().active()) {
return false;
if (!tg.config().active()) {
return false;
}
}
else {
lookups::RadioId target = m_network->m_ridLookup->find(dstId);
if ((!target.radioDefault() && !target.radioEnabled()) ||
(target.radioDefault() && m_network->m_rejectUnknownRID)) {
return false;
}
}
// repeat traffic to the connected peers
if (m_network->peerCount() > 0U) {
const uint32_t registeredPeerId = unitToUnit ? m_network->findPeerUnitReg(dstId) : 0U;
for (const auto& peer : m_network->peersSnapshot()) {
if (peerId != peer.first) {
if (registeredPeerId != 0U && peer.first != registeredPeerId)
continue;
write_Message_Grant(peer.first, srcId, dstId, 4U, !unitToUnit);
}
}
@ -1298,7 +1310,7 @@ bool TagNXDNData::write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t
bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag
uint8_t priority = ((serviceOptions & 0xFFU) & 0x07U); // Priority
std::unique_ptr<lc::rcch::MESSAGE_TYPE_VCALL_CONN> rcch = std::make_unique<lc::rcch::MESSAGE_TYPE_VCALL_CONN>();
std::unique_ptr<lc::rcch::MESSAGE_TYPE_VCALL_ASSGN> rcch = std::make_unique<lc::rcch::MESSAGE_TYPE_VCALL_ASSGN>();
// check the affiliations for this peer to see if we can grant traffic
std::shared_ptr<fne_lookups::AffiliationLookup> aff = m_network->getPeerAffiliations(peerId);
@ -1308,14 +1320,21 @@ bool TagNXDNData::write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t
return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior
}
else {
if (!aff->hasGroupAff(dstId)) {
if (grp && !aff->hasGroupAff(dstId)) {
return false;
}
if (!grp && !aff->isUnitReg(dstId)) {
TrafficNetwork::PeerConnectionPtr connection = m_network->findPeer(peerId);
if (connection == nullptr || !connection->isConventional())
return false;
}
}
rcch->setMessageType(MessageType::RCCH_VCALL_CONN);
rcch->setGrpVchNo(0U);
rcch->setGroup(grp);
rcch->setCallType(grp ? CallType::CONFERENCE : CallType::INDIVIDUAL);
rcch->setDuplex(false);
rcch->setTransmissionMode(TransmissionMode::MODE_4800);
rcch->setSrcId(srcId);
rcch->setDstId(dstId);

@ -464,9 +464,17 @@ void Slot::processNetwork(const data::NetData& dmrData)
break;
}
// validate the target ID, if the target is a talkgroup
if (!acl::AccessControl::validateTGId(dmrData.getSlotNo(), dmrData.getDstId())) {
break;
if (unitToUnit) {
// validate the target subscriber for an individual call
if (!acl::AccessControl::validateSrcId(dmrData.getDstId())) {
break;
}
}
else {
// validate the target talkgroup for a group call
if (!acl::AccessControl::validateTGId(dmrData.getSlotNo(), dmrData.getDstId())) {
break;
}
}
if (m_verbose) {
@ -595,6 +603,24 @@ void Slot::clock()
++it;
}
// ETSI TS 102 361-4 6.6.1.5: abandon an individual call when its
// called-party availability transaction expires
for (auto it = m_u2uOACSUTransactions.begin(); it != m_u2uOACSUTransactions.end(); ) {
if (it->second.expires <= now) {
m_control->writeRF_CSBK_NACK_RSP(it->second.callerId,
ReasonCode::TS_DENY_RSN_TGT_UNAVAILABLE, ServiceKind::IND_VOICE_CALL);
it = m_u2uOACSUTransactions.erase(it);
}
else {
if (it->second.nextRetry <= now) {
m_control->writeRF_CSBK_AHOY(it->second.calledId,
ServiceKind::IND_VOICE_CALL, it->second.callerId, false, 0U, true);
it->second.nextRetry = now + std::chrono::seconds(1U);
}
++it;
}
}
if (s_network != nullptr) {
if (s_network->getStatus() == network::NET_STAT_RUNNING) {
s_siteData.setNetActive(true);
@ -916,7 +942,7 @@ void Slot::grantTG(uint32_t srcId, uint32_t dstId, bool grp)
LogInfoEx(LOG_DMR, "DMR Slot %u, network TG grant demand, srcId = %u, dstId = %u", m_slotNo, srcId, dstId);
}
m_control->writeRF_CSBK_Grant(srcId, dstId, 4U, grp);
m_control->writeRF_CSBK_Grant(srcId, dstId, 4U, grp, true);
}
/* Releases a granted TG. */
@ -1901,6 +1927,100 @@ bool Slot::processUDTAcknowledgement(uint32_t targetId)
return false;
}
/* Starts a Tier III OACSU individual voice availability check. */
bool Slot::beginU2UOACSU(uint32_t callerId, uint32_t calledId, uint8_t serviceOptions)
{
constexpr uint32_t OACSU_TIMEOUT_SECONDS = 10U;
constexpr uint32_t OACSU_RETRY_SECONDS = 1U;
const auto now = std::chrono::steady_clock::now();
// check if there is an existing U2U OACSU transaction for the called party
auto calledIt = m_u2uOACSUTransactions.find(calledId);
if (calledIt != m_u2uOACSUTransactions.end() && calledIt->second.callerId != callerId) {
m_control->writeRF_CSBK_NACK_RSP(callerId,
ReasonCode::TS_DENY_RSN_TGT_BUSY, ServiceKind::IND_VOICE_CALL);
return false;
}
// if there is an existing transaction for the called party, send a wait response and an ahoy to the called party
if (calledIt != m_u2uOACSUTransactions.end()) {
m_control->writeRF_CSBK_ACK_RSP(callerId, ReasonCode::TS_WAIT_RSN, 0U);
m_control->writeRF_CSBK_AHOY(calledId, ServiceKind::IND_VOICE_CALL,
callerId, false, 0U, true);
return true;
}
// check if the caller has any existing U2U OACSU transactions with other called parties
for (const auto& transaction : m_u2uOACSUTransactions) {
if (transaction.second.callerId == callerId && transaction.second.calledId != calledId) {
m_control->writeRF_CSBK_NACK_RSP(callerId,
ReasonCode::TS_DENY_RSN_SYS_BUSY, ServiceKind::IND_VOICE_CALL);
return false;
}
}
// create a new U2U OACSU transaction for the called party
m_u2uOACSUTransactions[calledId] = {
callerId,
calledId,
serviceOptions,
now + std::chrono::seconds(OACSU_RETRY_SECONDS),
now + std::chrono::seconds(OACSU_TIMEOUT_SECONDS)
};
m_control->writeRF_CSBK_ACK_RSP(callerId, ReasonCode::TS_WAIT_RSN, 0U);
m_control->writeRF_CSBK_AHOY(calledId, ServiceKind::IND_VOICE_CALL,
callerId, false, 0U, true);
return true;
}
/* Completes a Tier III OACSU individual voice availability check. */
bool Slot::processU2UOACSUResponse(uint32_t calledId, uint32_t callerId, bool accepted)
{
// find the U2U OACSU transaction for the called party
auto it = m_u2uOACSUTransactions.find(calledId);
if (it == m_u2uOACSUTransactions.end() || it->second.callerId != callerId)
return false;
const U2UOACSUTransaction transaction = it->second;
// process the response for the U2U OACSU transaction
if (accepted) {
if (!m_control->writeRF_CSBK_Grant(transaction.callerId, transaction.calledId,
transaction.serviceOptions, false, false, false, 0U, 0U, true)) {
m_u2uOACSUTransactions.erase(it);
return true;
}
}
else {
m_control->writeRF_CSBK_NACK_RSP(transaction.callerId,
ReasonCode::TS_DENY_RSN_TGT_UNAVAILABLE, ServiceKind::IND_VOICE_CALL);
}
m_u2uOACSUTransactions.erase(it);
return true;
}
/* Cancels a pending Tier III OACSU individual voice availability check. */
bool Slot::cancelU2UOACSU(uint32_t callerId)
{
// cancel any pending U2U OACSU transactions for the caller
for (auto it = m_u2uOACSUTransactions.begin(); it != m_u2uOACSUTransactions.end(); ++it) {
if (it->second.callerId == callerId) {
m_control->writeRF_CSBK_AHOY(it->second.calledId, ServiceKind::CANCEL_CALL, callerId, false, 0U, true);
m_u2uOACSUTransactions.erase(it);
return true;
}
}
return false;
}
/* Helper to set the DMR short LC. */
void Slot::setShortLC(uint32_t slotNo, uint32_t id, FLCO::E flco, SLCO_ACT_TYPE actType)

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016,2017 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -373,26 +373,11 @@ namespace dmr
* @brief Represents an authentication transaction.
*/
struct AuthTransaction {
/**
* @brief Service options for the authentication transaction.
*/
uint8_t serviceOptions;
/**
* @brief Challenge value for the authentication transaction.
*/
uint32_t challenge;
/**
* @brief Expected response for the authentication transaction.
*/
uint32_t expectedResponse;
/**
* @brief Expiration time of the authentication transaction.
*/
std::chrono::steady_clock::time_point expires;
/**
* @brief Flag indicating whether the challenge has been sent.
*/
bool challengeSent;
uint8_t serviceOptions; //!< Service options for the authentication transaction.
uint32_t challenge; //!< Challenge value for the authentication transaction.
uint32_t expectedResponse; //!< Expected response for the authentication transaction.
std::chrono::steady_clock::time_point expires; //!< Expiration time of the authentication transaction.
bool challengeSent; //!< Flag indicating whether the challenge has been sent.
};
std::unordered_map<uint32_t, AuthTransaction> m_authTransactions;
@ -400,25 +385,26 @@ namespace dmr
* @brief Represents a Unified Data Transport (UDT) transaction.
*/
struct UDTTransaction {
/**
* @brief Target ID of the UDT transaction.
*/
uint32_t target;
/**
* @brief Indicates whether the UDT transaction is for a group.
*/
bool group;
/**
* @brief Service kind of the UDT transaction.
*/
uint8_t serviceKind;
/**
* @brief Expiration time of the UDT transaction.
*/
std::chrono::steady_clock::time_point expires;
uint32_t target; //!< Target ID of the UDT transaction.
bool group; //!< Indicates whether the UDT transaction is for a group.
uint8_t serviceKind; //!< Service kind of the UDT transaction.
std::chrono::steady_clock::time_point expires; //!< Expiration time of the UDT transaction.
};
std::unordered_map<uint32_t, UDTTransaction> m_udtTransactions;
/**
* @brief Represents a Tier III OACSU individual voice transaction.
* @ingroup host_dmr
*/
struct U2UOACSUTransaction {
uint32_t callerId; //!< Calling subscriber ID.
uint32_t calledId; //!< Called subscriber ID.
uint8_t serviceOptions; //!< Voice service options from the original request.
std::chrono::steady_clock::time_point nextRetry; //!< Time at which the availability enquiry is retransmitted.
std::chrono::steady_clock::time_point expires; //!< Expiration time of the availability check.
};
std::unordered_map<uint32_t, U2UOACSUTransaction> m_u2uOACSUTransactions;
uint32_t m_slotNo;
RingBuffer<uint8_t> m_txImmQueue;
@ -693,6 +679,29 @@ namespace dmr
*/
bool processUDTAcknowledgement(uint32_t targetId);
/**
* @brief Starts a Tier III OACSU individual voice availability check.
* @param callerId Calling subscriber ID.
* @param calledId Called subscriber ID.
* @param serviceOptions Voice service options.
* @return True when a new or duplicate transaction was accepted.
*/
bool beginU2UOACSU(uint32_t callerId, uint32_t calledId, uint8_t serviceOptions);
/**
* @brief Completes a pending OACSU availability check from a called-unit response.
* @param calledId Responding called subscriber ID.
* @param callerId Original calling subscriber ID.
* @param accepted Whether the called subscriber accepted the availability check.
* @return True when a matching transaction was processed.
*/
bool processU2UOACSUResponse(uint32_t calledId, uint32_t callerId, bool accepted);
/**
* @brief Cancels a pending OACSU transaction initiated by a caller.
* @param callerId Calling subscriber ID.
* @return True when a matching transaction was cancelled.
*/
bool cancelU2UOACSU(uint32_t callerId);
/**
* @brief Helper to set the DMR short LC.
* @param slotNo DMR slot number.

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016,2017,2018 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -208,15 +208,20 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len)
// verify the source RID is registered
VERIFY_SRCID_REG(csbk->toString(), ServiceKind::IND_VOICE_CALL, srcId);
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 1U);
if (m_slot->s_authoritative) {
writeRF_CSBK_Grant(srcId, dstId, isp->getServiceOptions(), false);
m_slot->beginU2UOACSU(srcId, dstId, isp->getServiceOptions());
} else {
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 0U);
if (m_slot->s_network != nullptr)
m_slot->s_network->writeGrantReq(modem::DVM_STATE::STATE_DMR, srcId, dstId, m_slot->m_slotNo, true);
}
break;
case ServiceKind::CANCEL_CALL:
if (m_slot->cancelU2UOACSU(srcId))
writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_ACK_RSN_MSG, 0U);
else
writeRF_CSBK_NACK_RSP(srcId, ReasonCode::TS_DENY_RSN_CALL_CNCL_REFUSED, ServiceKind::CANCEL_CALL);
break;
case ServiceKind::GRP_VOICE_CALL:
// make sure control data is supported
IS_SUPPORT_CONTROL_CHECK(csbk->toString(), ServiceKind::GRP_VOICE_CALL, srcId);
@ -319,12 +324,16 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len)
case CSBKO::ACK_RSP_U:
{
VERBOSE_LOG_CSBK(csbk->toString(), srcId, dstId);
::ActivityLog("DMR", true, "Slot %u ack response from %u to %u", m_slot->m_slotNo, srcId, dstId);
CSBK_ACK_RSP* ack = static_cast<CSBK_ACK_RSP*>(csbk.get());
if (csbk->getCSBKO() == CSBKO::ACK_RSP_U && ack->getReason() == ReasonCode::MS_ACK_RSN_AUTH_RESP)
m_slot->processRegistrationAuthResponse(srcId, dstId);
else if (csbk->getCSBKO() == CSBKO::ACK_RSP_U && ack->getReason() == ReasonCode::MS_ACK_RSN_MSG)
m_slot->processUDTAcknowledgement(srcId);
else if (csbk->getCSBKO() == CSBKO::ACK_RSP_U && ack->getReason() == ReasonCode::MS_ACK_RSN_MSG) {
if (!m_slot->processU2UOACSUResponse(srcId, dstId, true))
m_slot->processUDTAcknowledgement(srcId);
}
}
break;
case CSBKO::EXT_FNCT:
@ -364,6 +373,7 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len)
case CSBKO::NACK_RSP:
{
VERBOSE_LOG_CSBK(csbk->toString(), srcId, dstId);
m_slot->processU2UOACSUResponse(srcId, dstId, false);
}
break;
case CSBKO::MAINT:
@ -581,9 +591,15 @@ void ControlSignaling::processNetwork(const data::NetData& dmrData)
}
break;
case CSBKO::ACK_RSP:
case CSBKO::ACK_RSP_U:
{
VERBOSE_LOG_CSBK_NET(csbk->toString(), srcId, dstId);
::ActivityLog("DMR", false, "Slot %u ack response from %u to %u", m_slot->m_slotNo, srcId, dstId);
CSBK_ACK_RSP* ack = static_cast<CSBK_ACK_RSP*>(csbk.get());
if (csbk->getCSBKO() == CSBKO::ACK_RSP_U && ack->getReason() == ReasonCode::MS_ACK_RSN_MSG)
m_slot->processU2UOACSUResponse(srcId, dstId, true);
}
break;
case CSBKO::EXT_FNCT:
@ -623,6 +639,7 @@ void ControlSignaling::processNetwork(const data::NetData& dmrData)
case CSBKO::NACK_RSP:
{
VERBOSE_LOG_CSBK_NET(csbk->toString(), srcId, dstId);
m_slot->processU2UOACSUResponse(srcId, dstId, false);
}
break;
case CSBKO::PRECCSBK:
@ -851,8 +868,8 @@ void ControlSignaling::writeRF_CSBK_ACK_RSP(uint32_t dstId, uint8_t reason, uint
/* Helper to write an AHOY packet. */
void ControlSignaling::writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind,
uint32_t sourceOrChallenge, bool gi, uint8_t appendedBlocks)
void ControlSignaling::writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind, uint32_t sourceOrChallenge, bool gi,
uint8_t appendedBlocks, bool network)
{
std::unique_ptr<CSBK_AHOY> csbk = std::make_unique<CSBK_AHOY>();
csbk->setServiceKind(serviceKind);
@ -871,6 +888,8 @@ void ControlSignaling::writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind,
}
writeRF_CSBK_Imm(csbk.get());
if (network)
writeNet_CSBK(csbk.get());
}
/* Helper to write a NACK RSP packet. */
@ -894,7 +913,8 @@ void ControlSignaling::writeRF_CSBK_NACK_RSP(uint32_t dstId, uint8_t reason, uin
/* Helper to write a grant packet. */
bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp, bool net, bool skip, uint32_t chNo, uint8_t preferredSlot)
bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions,
bool grp, bool net, bool skip, uint32_t chNo, uint8_t preferredSlot, bool targetConfirmed)
{
Slot* tscc = m_slot->s_dmr->getTSCCSlot();
@ -949,7 +969,17 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
if (!tscc->s_affiliations->isGranted(dstId)) {
::lookups::TalkgroupRuleGroupVoice groupVoice = tscc->s_tidLookup->find(dstId);
slot = preferredSlot != 0U ? preferredSlot : groupVoice.source().tgSlot();
if (preferredSlot != 0U) {
slot = preferredSlot;
}
else if (grp) {
slot = groupVoice.source().tgSlot();
}
else {
// Individual calls have no talkgroup rule from which to obtain
// a preferred slot. Prefer the payload slot opposite the TSCC.
slot = tscc->m_slotNo == 1U ? 2U : 1U;
}
if (grp && !tscc->m_ignoreAffiliationCheck) {
// is this an affiliation required group?
@ -962,7 +992,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
}
if (!grp && !tscc->m_ignoreAffiliationCheck) {
if (!grp && !targetConfirmed && !tscc->m_ignoreAffiliationCheck) {
// is this the target registered?
if (!tscc->s_affiliations->isUnitReg(dstId)) {
LogWarning(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access, IND_VOICE_CALL (Individual Voice Call) ignored, no unit registration, dstId = %u", tscc->m_slotNo, dstId);
@ -987,6 +1017,11 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
uint32_t availChNo = chNo != 0U ? chNo : tscc->s_affiliations->getAvailableChannelForSlot(slot);
if (!grp && preferredSlot == 0U && availChNo == 0U) {
slot = slot == 1U ? 2U : 1U;
availChNo = tscc->s_affiliations->getAvailableChannelForSlot(slot);
}
if (availChNo == 0U) {
if (grp) {
if (!net) {
@ -1222,6 +1257,14 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
for (uint8_t i = 0; i < 2U; i++)
writeRF_CSBK_Imm(csbk.get());
// Tier III individual assignment is required at both subscriber
// sites; ask the FNE to demand a site-local grant for the called
// subscriber rather than forwarding this site's channel number
if (!net && tscc->s_network != nullptr) {
tscc->s_network->writeGrantReq(modem::DVM_STATE::STATE_DMR,
srcId, dstId, slot, true);
}
// if the channel granted isn't the same as the TSCC; remote activate the payload channel
if (chNo != tscc->s_channelNo) {
::lookups::VoiceChData voiceChData = tscc->s_affiliations->rfCh()->getRFChData(chNo);

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016,2017 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2022 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -146,8 +146,10 @@ namespace dmr
* @param sourceOrChallenge Source or Challenge ID.
* @param gi Flag indicating whether the GI bit is set.
* @param appendedBlocks Number of appended blocks.
* @param network Flag indicating the enquiry is also sent over the FNE network.
*/
void writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind, uint32_t sourceOrChallenge, bool gi, uint8_t appendedBlocks);
void writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind, uint32_t sourceOrChallenge, bool gi,
uint8_t appendedBlocks, bool network = false);
/**
* @brief Helper to write a NACK RSP packet.
* @param dstId Destination ID.
@ -164,9 +166,10 @@ namespace dmr
* @param net Flag indicating this grant is coming from network traffic.
* @param skip Flag indicating normal grant checking is skipped.
* @param chNo Channel Number.
* @param targetConfirmed Flag indicating an OACSU response confirmed the remote target.
*/
bool writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp, bool net = false, bool skip = false,
uint32_t chNo = 0U, uint8_t preferredSlot = 0U);
uint32_t chNo = 0U, uint8_t preferredSlot = 0U, bool targetConfirmed = false);
/**
* @brief Helper to write a data grant packet.
* @param srcId Source Radio ID.

@ -143,6 +143,17 @@ bool Voice::process(uint8_t* data, uint32_t len)
}
}
}
else if (!acl::AccessControl::validateSrcId(dstId)) {
LogWarning(LOG_RF, "DMR Slot %u, VOICE_LC_HEADER denial, destination RID rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, srcId, dstId);
::ActivityLog("DMR", true, "Slot %u RF private voice rejection from %u to %u ", m_slot->m_slotNo, srcId, dstId);
m_slot->m_rfLastDstId = 0U;
m_slot->m_rfLastSrcId = 0U;
m_slot->m_rfTGHang.stop();
m_slot->m_rfState = RS_RF_REJECTED;
return false;
}
m_slot->m_data->m_lastRejectId = 0U;
@ -608,6 +619,11 @@ bool Voice::process(uint8_t* data, uint32_t len)
return false;
}
}
else if (!acl::AccessControl::validateSrcId(dstId)) {
LogWarning(LOG_RF, "DMR Slot %u, VOICE denial, destination RID rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, srcId, dstId);
m_slot->m_rfState = RS_RF_REJECTED;
return false;
}
m_slot->m_rfLC = std::move(lc);

@ -70,6 +70,7 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q
m_duplex(duplex),
m_enableControl(false),
m_dedicatedControl(false),
m_unitToUnitAvailCheck(false),
m_ignoreAffiliationCheck(false),
m_legacyGroupReg(false),
m_defaultNetIdleTalkgroup(0U),
@ -218,6 +219,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
}
m_control->m_disableGrantSrcIdCheck = control["disableGrantSourceIdCheck"].as<bool>(false);
m_unitToUnitAvailCheck = control["unitToUnitAvailCheck"].as<bool>(false);
m_ccDebug = control["debug"].as<bool>(false);
m_ignoreAffiliationCheck = nxdnProtocol["ignoreAffiliationCheck"].as<bool>(false);
@ -597,6 +599,8 @@ void Control::clock()
uint32_t ms = m_interval.elapsed();
m_interval.start();
m_control->clock();
if (m_network != nullptr) {
processNetwork();

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015-2020 Jonathan Naylor, G4KLX
* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -289,6 +289,7 @@ namespace nxdn
bool m_duplex;
bool m_enableControl;
bool m_dedicatedControl;
bool m_unitToUnitAvailCheck;
bool m_ignoreAffiliationCheck;
bool m_legacyGroupReg;

@ -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-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -168,11 +168,17 @@ bool ControlSignaling::process(FuncChannelType::E fct, ChOption::E option, uint8
// validate the source RID
VALID_SRCID(rcch->toString(true), MessageType::RTCH_VCALL, srcId, CauseResponse::VD_REQ_UNIT_NOT_PERM);
// validate the talkgroup ID
VALID_TGID(rcch->toString(true), MessageType::RTCH_VCALL, dstId, srcId, CauseResponse::VD_TGT_UNIT_NOT_PERM);
// verify the source RID is affiliated
VERIFY_SRCID_AFF(rcch->toString(true), MessageType::RTCH_VCALL, srcId, dstId, CauseResponse::VD_REQ_UNIT_NOT_REG);
const bool group = rcch->getGroup();
if (group) {
// validate the talkgroup ID and source affiliation
VALID_TGID(rcch->toString(true), MessageType::RTCH_VCALL, dstId, srcId, CauseResponse::VD_TGT_UNIT_NOT_PERM);
VERIFY_SRCID_AFF(rcch->toString(true), MessageType::RTCH_VCALL, srcId, dstId, CauseResponse::VD_REQ_UNIT_NOT_REG);
}
else {
// validate the target subscriber for an individual call
VALID_DSTID(rcch->toString(true), MessageType::RTCH_VCALL, dstId, srcId, CauseResponse::VD_TGT_UNIT_NOT_PERM);
VERIFY_SRCID_REG(rcch->toString(true), MessageType::RTCH_VCALL, srcId, CauseResponse::VD_REQ_UNIT_NOT_REG);
}
VERBOSE_LOG_MSG(rcch->toString(true), srcId, dstId);
uint8_t serviceOptions = (rcch->getEmergency() ? 0x80U : 0x00U) + // Emergency Flag
@ -180,13 +186,50 @@ bool ControlSignaling::process(FuncChannelType::E fct, ChOption::E option, uint8
(rcch->getPriority() & 0x07U); // Priority
if (m_nxdn->m_authoritative) {
writeRF_Message_Grant(srcId, dstId, serviceOptions, true);
if (!group && m_nxdn->m_unitToUnitAvailCheck)
beginU2UAvailability(srcId, dstId, serviceOptions);
else
writeRF_Message_Grant(srcId, dstId, serviceOptions, group);
} else {
if (m_nxdn->m_network != nullptr)
m_nxdn->m_network->writeGrantReq(modem::DVM_STATE::STATE_NXDN, srcId, dstId, 0U, false);
m_nxdn->m_network->writeGrantReq(modem::DVM_STATE::STATE_NXDN, srcId, dstId, 0U, !group);
}
}
break;
case MessageType::RCCH_VCALL_REC:
{
const bool accepted = rcch->getCauseResponse() == CauseResponse::VD_ACCEPTED;
if (!completeU2UAvailability(srcId, dstId, accepted))
writeNetwork(data, len);
}
break;
case MessageType::RCCH_VCALL_CONN:
{
if (!completeU2UAvailability(srcId, dstId, true))
writeNetwork(data, len);
}
break;
case MessageType::DISC:
{
cancelU2U(srcId);
uint32_t grantDstId = m_nxdn->m_affiliations->isGranted(srcId) ? srcId : m_nxdn->m_affiliations->getGrantedBySrcId(srcId);
if (grantDstId == 0U && m_nxdn->m_affiliations->isGranted(dstId))
grantDstId = dstId;
if (grantDstId != 0U)
m_nxdn->m_affiliations->releaseGrant(grantDstId, false);
// create and send a disconnect message on the RF interface
std::unique_ptr<MESSAGE_TYPE_VCALL_CONN> disconnect = std::make_unique<MESSAGE_TYPE_VCALL_CONN>();
disconnect->setMessageType(MessageType::DISC);
disconnect->setCallType(CallType::INDIVIDUAL);
disconnect->setGroup(false);
disconnect->setSrcId(srcId);
disconnect->setDstId(dstId);
disconnect->setCauseResponse(CauseResponse::DREQ_USER);
writeRF_Message(disconnect.get(), false, true);
}
break;
case MessageType::RCCH_REG:
{
// make sure control data is supported
@ -261,13 +304,33 @@ bool ControlSignaling::processNetwork(FuncChannelType::E fct, ChOption::E option
(rcch->getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag
(rcch->getPriority() & 0x07U); // Priority
writeRF_Message_Grant(srcId, dstId, serviceOptions, true, true);
writeRF_Message_Grant(srcId, dstId, serviceOptions, rcch->getGroup(), true);
}
}
}
return true; // don't allow this to write to the air
case MessageType::RCCH_VCALL_REC:
{
const bool accepted = rcch->getCauseResponse() == CauseResponse::VD_ACCEPTED;
if (completeU2UAvailability(srcId, dstId, accepted))
return true;
}
break;
case MessageType::RCCH_VCALL_CONN:
break; // the FNE may explicitly send these
if (completeU2UAvailability(srcId, dstId, true))
return true;
break;
case MessageType::DISC:
{
cancelU2U(srcId);
uint32_t grantDstId = m_nxdn->m_affiliations->isGranted(srcId) ? srcId : m_nxdn->m_affiliations->getGrantedBySrcId(srcId);
if (grantDstId == 0U && m_nxdn->m_affiliations->isGranted(dstId))
grantDstId = dstId;
if (grantDstId != 0U)
m_nxdn->m_affiliations->releaseGrant(grantDstId, false);
}
break;
default:
LogError(LOG_NET, "NXDN, unhandled message type, messageType = $%02X", rcch->getMessageType());
return false;
@ -279,6 +342,37 @@ bool ControlSignaling::processNetwork(FuncChannelType::E fct, ChOption::E option
return true;
}
/* Expires pending Type-C call setup transactions. */
void ControlSignaling::clock()
{
// expire pending Type-C call setup transactions based on the current time
const auto now = std::chrono::steady_clock::now();
for (auto it = m_u2uTransactions.begin(); it != m_u2uTransactions.end();) {
if (it->second.expires <= now) {
writeRF_Message_Deny(0U, it->second.callerId,
CauseResponse::VD_TGT_UNIT_NOT_PERM, MessageType::RTCH_VCALL);
it = m_u2uTransactions.erase(it);
}
else {
++it;
}
}
// update grants on the RF interface if the last update was more than 500ms ago
if (now - m_lastGrantUpdate >= std::chrono::milliseconds(500)) {
const std::unordered_map<uint32_t, uint32_t> grants =
m_nxdn->m_affiliations->grantTable();
for (const auto& grant : grants) {
const uint32_t dstId = grant.first;
writeRF_Message_Grant(m_nxdn->m_affiliations->getGrantedSrcId(dstId),
dstId, 4U, m_nxdn->m_affiliations->isGroup(dstId), true, true,
grant.second, true);
}
m_lastGrantUpdate = now;
}
}
// ---------------------------------------------------------------------------
// Protected Class Members
// ---------------------------------------------------------------------------
@ -297,6 +391,8 @@ ControlSignaling::ControlSignaling(Control* nxdn, bool debug, bool verbose) :
m_verifyAff(false),
m_verifyReg(false),
m_disableGrantSrcIdCheck(false),
m_u2uTransactions(),
m_lastGrantUpdate(std::chrono::steady_clock::now()),
m_lastRejectId(0U),
m_verbose(verbose),
m_debug(debug)
@ -450,13 +546,16 @@ void ControlSignaling::writeRF_ControlData(uint8_t n, bool first)
/* Helper to write a grant packet. */
bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp, bool net, bool skip, uint32_t chNo)
bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId,
uint8_t serviceOptions, bool grp, bool net, bool skip, uint32_t chNo, bool update)
{
bool emergency = ((serviceOptions & 0xFFU) & 0x80U) == 0x80U; // Emergency Flag
bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag
uint8_t priority = ((serviceOptions & 0xFFU) & 0x07U); // Priority
std::unique_ptr<rcch::MESSAGE_TYPE_VCALL_ASSGN> rcch = std::make_unique<rcch::MESSAGE_TYPE_VCALL_ASSGN>();
if (update)
rcch->setMessageType(MessageType::RCCH_VCALL_ASSGN_DUP);
// are we skipping checking?
if (!skip) {
@ -594,7 +693,8 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
else {
if (m_nxdn->m_affiliations->isGranted(dstId)) {
chNo = m_nxdn->m_affiliations->getGrantedCh(dstId);
m_nxdn->m_affiliations->touchGrant(dstId);
if (!update)
m_nxdn->m_affiliations->touchGrant(dstId);
}
else {
return false;
@ -693,6 +793,7 @@ void ControlSignaling::writeRF_Message_Deny(uint32_t srcId, uint32_t dstId, uint
case MessageType::RTCH_VCALL:
rcch = std::make_unique<rcch::MESSAGE_TYPE_VCALL_CONN>();
rcch->setMessageType(MessageType::RTCH_VCALL);
break;
default:
return;
}
@ -957,3 +1058,87 @@ void ControlSignaling::writeRF_CC_Message_Idle()
m_nxdn->addFrame(data);
}
}
/* Starts a Type-C individual-call availability check. */
bool ControlSignaling::beginU2UAvailability(uint16_t callerId, uint16_t calledId, uint8_t serviceOptions)
{
constexpr uint32_t AVAILABILITY_TIMEOUT_SECONDS = 5U;
// check if the called subscriber is already involved in another pending U2U transaction
auto calledIt = m_u2uTransactions.find(calledId);
if (calledIt != m_u2uTransactions.end() && calledIt->second.callerId != callerId) {
writeRF_Message_Deny(0U, callerId, CauseResponse::VD_QUE_TGT_UNIT_BUSY, MessageType::RTCH_VCALL);
return false;
}
// check if the caller is already involved in another pending U2U transaction
for (const auto& transaction : m_u2uTransactions) {
if (transaction.second.callerId == callerId && transaction.second.calledId != calledId) {
writeRF_Message_Deny(0U, callerId, CauseResponse::VD_QUE_GRP_BUSY, MessageType::RTCH_VCALL);
return false;
}
}
m_u2uTransactions[calledId] = {
callerId,
calledId,
serviceOptions,
std::chrono::steady_clock::now() + std::chrono::seconds(AVAILABILITY_TIMEOUT_SECONDS)
};
writeRF_Message_Deny(0U, callerId, CauseResponse::VD_QUE_CHN_RESOURCE_NOT_AVAIL, MessageType::RTCH_VCALL);
// create and send the RCCH individual-call availability request
std::unique_ptr<MESSAGE_TYPE_VCALL_CONN> request = std::make_unique<MESSAGE_TYPE_VCALL_CONN>();
request->setMessageType(MessageType::RCCH_VCALL_REC);
request->setCallType(CallType::INDIVIDUAL);
request->setGroup(false);
request->setSrcId(callerId);
request->setDstId(calledId);
writeRF_Message(request.get(), false, true);
return true;
}
/* Completes a Type-C individual-call availability check. */
bool ControlSignaling::completeU2UAvailability(uint16_t calledId, uint16_t callerId, bool accepted)
{
// find the pending U2U transaction for the called subscriber ID
auto it = m_u2uTransactions.find(calledId);
if (it == m_u2uTransactions.end() || it->second.callerId != callerId)
return false;
// store the transaction for easier access and readability
const U2UTransaction transaction = it->second;
if (accepted) {
if (!writeRF_Message_Grant(transaction.callerId, transaction.calledId,
transaction.serviceOptions, false)) {
return true;
}
}
else {
// if the called subscriber is not available, send a deny message
writeRF_Message_Deny(0U, transaction.callerId, CauseResponse::VD_TGT_UNIT_NOT_PERM, MessageType::RTCH_VCALL);
}
m_u2uTransactions.erase(it);
return true;
}
/* Cancels pending Type-C individual-call state. */
bool ControlSignaling::cancelU2U(uint16_t unitId)
{
// iterate through all pending U2U transactions and remove any involving the specified unit ID
for (auto it = m_u2uTransactions.begin(); it != m_u2uTransactions.end(); ++it) {
if (it->second.callerId == unitId || it->second.calledId == unitId) {
m_u2uTransactions.erase(it);
return true;
}
}
return false;
}

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2022-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -21,7 +21,9 @@
#include "nxdn/Control.h"
#include <cstdio>
#include <chrono>
#include <string>
#include <unordered_map>
namespace nxdn
{
@ -67,6 +69,11 @@ namespace nxdn
bool processNetwork(defines::FuncChannelType::E fct, defines::ChOption::E option, lc::RTCH& netLC, uint8_t* data, uint32_t len);
/** @} */
/**
* @brief Expires pending Type-C call setup transactions.
*/
void clock();
protected:
friend class nxdn::packet::Data;
friend class nxdn::packet::Voice;
@ -87,6 +94,19 @@ namespace nxdn
bool m_disableGrantSrcIdCheck;
/**
* @brief Pending Type-C individual-call availability transaction.
* @ingroup host_nxdn
*/
struct U2UTransaction {
uint16_t callerId; //!< Calling subscriber ID.
uint16_t calledId; //!< Called subscriber ID.
uint8_t serviceOptions; //!< Original voice service options.
std::chrono::steady_clock::time_point expires; //!< Availability-response deadline.
};
std::unordered_map<uint16_t, U2UTransaction> m_u2uTransactions;
std::chrono::steady_clock::time_point m_lastGrantUpdate;
uint16_t m_lastRejectId;
bool m_verbose;
@ -145,7 +165,9 @@ namespace nxdn
* @param chNo Channel Number.
* @returns True, if granted, otherwise false.
*/
bool writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp, bool net = false, bool skip = false, uint32_t chNo = 0U);
bool writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOptions,
bool grp, bool net = false, bool skip = false, uint32_t chNo = 0U,
bool update = false);
/**
* @brief Helper to write a deny packet.
* @param srcId Source Radio ID.
@ -181,6 +203,29 @@ namespace nxdn
* @brief Helper to write a CC IDLE broadcast packet on the RF interface.
*/
void writeRF_CC_Message_Idle();
/**
* @brief Starts a Type-C individual-call availability check.
* @param callerId Calling subscriber ID.
* @param calledId Called subscriber ID.
* @param serviceOptions Voice service options.
* @return True when the transaction was accepted.
*/
bool beginU2UAvailability(uint16_t callerId, uint16_t calledId, uint8_t serviceOptions);
/**
* @brief Completes a Type-C individual-call availability check.
* @param calledId Responding subscriber ID.
* @param callerId Calling subscriber ID.
* @param accepted Whether the called subscriber is available.
* @return True when a matching transaction was found.
*/
bool completeU2UAvailability(uint16_t calledId, uint16_t callerId, bool accepted);
/**
* @brief Cancels pending individual-call state involving a subscriber.
* @param unitId Subscriber requesting cancellation.
* @return True when state was cancelled.
*/
bool cancelU2U(uint16_t unitId);
};
} // namespace packet
} // namespace nxdn

@ -834,6 +834,73 @@ TEST_CASE("DMR Tier III re-key on a retained payload channel recreates its grant
CHECK(HostTestHooks::dmrPayloadDstId(*payload) == 0U);
}
TEST_CASE("DMR Tier III OACSU grants only after the called unit accepts",
"[dmr][tier3][host][control][u2u][oacsu]")
{
DMRHostHarness harness;
constexpr uint32_t CALLER_ID = 1001U;
constexpr uint32_t CALLED_ID = 1002U;
REQUIRE(harness.m_chLookup.initializeRFCh(101U));
HostTestHooks::dmrEnableTier3(*harness.m_control, 101U, 1U);
dmr::Slot* tscc = HostTestHooks::dmrSlot1(*harness.m_control);
HostTestHooks::dmrRegisterUnit(*tscc, CALLER_ID);
HostTestHooks::dmrRegisterUnit(*tscc, CALLED_ID);
HostTestHooks::dmrBeginU2UOACSU(*tscc, CALLER_ID, CALLED_ID, 4U);
CHECK(HostTestHooks::dmrPendingU2UOACSU(*tscc) == 1U);
CHECK_FALSE(HostTestHooks::dmrIsGranted(*tscc, CALLED_ID));
REQUIRE(HostTestHooks::dmrRespondU2UOACSU(*tscc, CALLED_ID, CALLER_ID, true));
CHECK(HostTestHooks::dmrPendingU2UOACSU(*tscc) == 0U);
CHECK(HostTestHooks::dmrIsGranted(*tscc, CALLED_ID));
CHECK_FALSE(HostTestHooks::dmrIsGroupGrant(*tscc, CALLED_ID));
}
TEST_CASE("DMR Tier III OACSU rejection and cancellation do not create grants",
"[dmr][tier3][host][control][u2u][oacsu]")
{
DMRHostHarness harness;
constexpr uint32_t CALLER_ID = 1001U;
constexpr uint32_t CALLED_ID = 1002U;
REQUIRE(harness.m_chLookup.initializeRFCh(101U));
HostTestHooks::dmrEnableTier3(*harness.m_control, 101U, 1U);
dmr::Slot* tscc = HostTestHooks::dmrSlot1(*harness.m_control);
HostTestHooks::dmrRegisterUnit(*tscc, CALLER_ID);
HostTestHooks::dmrRegisterUnit(*tscc, CALLED_ID);
HostTestHooks::dmrBeginU2UOACSU(*tscc, CALLER_ID, CALLED_ID);
REQUIRE(HostTestHooks::dmrRespondU2UOACSU(*tscc, CALLED_ID, CALLER_ID, false));
CHECK_FALSE(HostTestHooks::dmrIsGranted(*tscc, CALLED_ID));
HostTestHooks::dmrBeginU2UOACSU(*tscc, CALLER_ID, CALLED_ID);
REQUIRE(HostTestHooks::dmrCancelU2UOACSU(*tscc, CALLER_ID));
CHECK(HostTestHooks::dmrPendingU2UOACSU(*tscc) == 0U);
CHECK_FALSE(HostTestHooks::dmrIsGranted(*tscc, CALLED_ID));
}
TEST_CASE("DMR Tier III OACSU prevents competing private-call transactions",
"[dmr][tier3][host][control][u2u][oacsu]")
{
DMRHostHarness harness;
constexpr uint32_t FIRST_CALLER_ID = 1001U;
constexpr uint32_t SECOND_CALLER_ID = 1002U;
constexpr uint32_t CALLED_ID = 1003U;
constexpr uint32_t OTHER_CALLED_ID = 1004U;
REQUIRE(harness.m_chLookup.initializeRFCh(101U));
HostTestHooks::dmrEnableTier3(*harness.m_control, 101U, 1U);
dmr::Slot* tscc = HostTestHooks::dmrSlot1(*harness.m_control);
REQUIRE(HostTestHooks::dmrBeginU2UOACSU(*tscc, FIRST_CALLER_ID, CALLED_ID));
CHECK_FALSE(HostTestHooks::dmrBeginU2UOACSU(*tscc, SECOND_CALLER_ID, CALLED_ID));
CHECK_FALSE(HostTestHooks::dmrBeginU2UOACSU(*tscc, FIRST_CALLER_ID, OTHER_CALLED_ID));
CHECK(HostTestHooks::dmrPendingU2UOACSU(*tscc) == 1U);
CHECK_FALSE(HostTestHooks::dmrIsGranted(*tscc, CALLED_ID));
CHECK_FALSE(HostTestHooks::dmrIsGranted(*tscc, OTHER_CALLED_ID));
}
TEST_CASE("DMR rfTGHang expiry ends active RF call and returns slot to listening", "[dmr][host][control][rf]")
{
DMRHostHarness harness;

@ -589,6 +589,112 @@ TEST_CASE("FNE P25 uses the explicit U2U flag for late-entry voice", "[fne][call
harness.traffic, PEER_ID, SOURCE_ID, DESTINATION_ID));
}
TEST_CASE("FNE DMR individual grant demand targets the called unit site",
"[fne][callhandler][routing][u2u][dmr]")
{
constexpr uint32_t SOURCE_PEER = 1001U;
constexpr uint32_t SOURCE_SITE = 1002U;
constexpr uint32_t TARGET_SITE = 1003U;
constexpr uint32_t OTHER_SITE = 1004U;
constexpr uint32_t SOURCE_ID = 2001U;
constexpr uint32_t DESTINATION_ID = 2002U;
FNEHarness harness;
FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, SOURCE_SITE, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, TARGET_SITE, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, OTHER_SITE, network::NET_STAT_RUNNING, true);
harness.radios.addEntry(SOURCE_ID, true, "test source");
harness.radios.addEntry(DESTINATION_ID, true, "test destination");
FNETestHooks::registerP25Unit(harness.traffic, SOURCE_SITE, SOURCE_ID);
FNETestHooks::registerP25Unit(harness.traffic, TARGET_SITE, DESTINATION_ID);
std::vector<uint32_t> writes;
FNETestHooks::capturePeerWrites(harness.traffic, writes);
REQUIRE(FNETestHooks::processDMRGrantRequest(
harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true));
CHECK(writes == std::vector<uint32_t>{ TARGET_SITE });
}
TEST_CASE("FNE DMR group grant demand retains group peer routing",
"[fne][callhandler][routing][group][dmr]")
{
constexpr uint32_t SOURCE_PEER = 1001U;
constexpr uint32_t SITE_A = 1002U;
constexpr uint32_t SITE_B = 1003U;
constexpr uint32_t SOURCE_ID = 2001U;
constexpr uint32_t TALK_GROUP = 3100U;
FNEHarness harness;
FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, SITE_A, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, SITE_B, network::NET_STAT_RUNNING, true);
harness.radios.addEntry(SOURCE_ID, true, "test source");
harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false);
FNETestHooks::addGroupAffiliation(harness.traffic, SITE_A, SOURCE_ID, TALK_GROUP);
FNETestHooks::addGroupAffiliation(harness.traffic, SITE_B, SOURCE_ID, TALK_GROUP);
std::vector<uint32_t> writes;
FNETestHooks::capturePeerWrites(harness.traffic, writes);
REQUIRE(FNETestHooks::processDMRGrantRequest(
harness.traffic, SOURCE_PEER, SOURCE_ID, TALK_GROUP, false));
std::sort(writes.begin(), writes.end());
CHECK(writes == std::vector<uint32_t>{ SITE_A, SITE_B });
}
TEST_CASE("FNE NXDN individual grant demand targets the called unit site",
"[fne][callhandler][routing][u2u][nxdn]")
{
constexpr uint32_t SOURCE_PEER = 1001U;
constexpr uint32_t SOURCE_SITE = 1002U;
constexpr uint32_t TARGET_SITE = 1003U;
constexpr uint32_t OTHER_SITE = 1004U;
constexpr uint32_t SOURCE_ID = 2001U;
constexpr uint32_t DESTINATION_ID = 2002U;
FNEHarness harness;
FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, SOURCE_SITE, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, TARGET_SITE, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, OTHER_SITE, network::NET_STAT_RUNNING, true);
harness.radios.addEntry(SOURCE_ID, true, "test source");
harness.radios.addEntry(DESTINATION_ID, true, "test destination");
FNETestHooks::registerP25Unit(harness.traffic, SOURCE_SITE, SOURCE_ID);
FNETestHooks::registerP25Unit(harness.traffic, TARGET_SITE, DESTINATION_ID);
std::vector<uint32_t> writes;
FNETestHooks::capturePeerWrites(harness.traffic, writes);
REQUIRE(FNETestHooks::processNXDNGrantRequest(
harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true));
CHECK(writes == std::vector<uint32_t>{ TARGET_SITE });
}
TEST_CASE("FNE NXDN group grant demand retains group peer routing",
"[fne][callhandler][routing][group][nxdn]")
{
constexpr uint32_t SOURCE_PEER = 1001U;
constexpr uint32_t SITE_A = 1002U;
constexpr uint32_t SITE_B = 1003U;
constexpr uint32_t SOURCE_ID = 2001U;
constexpr uint32_t TALK_GROUP = 3100U;
FNEHarness harness;
FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, SITE_A, network::NET_STAT_RUNNING, true);
FNETestHooks::addPeer(harness.traffic, SITE_B, network::NET_STAT_RUNNING, true);
harness.radios.addEntry(SOURCE_ID, true, "test source");
harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false);
FNETestHooks::addGroupAffiliation(harness.traffic, SITE_A, SOURCE_ID, TALK_GROUP);
FNETestHooks::addGroupAffiliation(harness.traffic, SITE_B, SOURCE_ID, TALK_GROUP);
std::vector<uint32_t> writes;
FNETestHooks::capturePeerWrites(harness.traffic, writes);
REQUIRE(FNETestHooks::processNXDNGrantRequest(
harness.traffic, SOURCE_PEER, SOURCE_ID, TALK_GROUP, false));
std::sort(writes.begin(), writes.end());
CHECK(writes == std::vector<uint32_t>{ SITE_A, SITE_B });
}
TEST_CASE("FNE call handlers validate source radios and group state", "[fne][callhandler][validation]")
{
constexpr uint32_t PEER_ID = 1001U;

@ -299,6 +299,24 @@ bool FNETestHooks::validateP25IndividualCall(TrafficNetwork& network, uint32_t p
peerId, control, p25::defines::DUID::LDU2, STREAM_ID, false);
}
/* Processes a synthetic DMR FNE grant request. */
bool FNETestHooks::processDMRGrantRequest(TrafficNetwork& network, uint32_t peerId,
uint32_t sourceId, uint32_t destinationId, bool unitToUnit)
{
return network.m_tagDMR->processGrantReq(
sourceId, destinationId, 1U, unitToUnit, peerId, 1U, 105U);
}
/* Processes a synthetic NXDN FNE grant request. */
bool FNETestHooks::processNXDNGrantRequest(TrafficNetwork& network, uint32_t peerId,
uint32_t sourceId, uint32_t destinationId, bool unitToUnit)
{
return network.m_tagNXDN->processGrantReq(
sourceId, destinationId, unitToUnit, peerId, 1U, 106U);
}
/* Sets whether unit-to-unit traffic is globally disabled. */
void FNETestHooks::disallowU2U(TrafficNetwork& network, bool disabled)

@ -174,6 +174,28 @@ public:
*/
static bool validateP25IndividualCall(network::TrafficNetwork& network, uint32_t peerId,
uint32_t sourceId, uint32_t destinationId);
/**
* @brief Processes a synthetic DMR FNE grant request.
* @param network The TrafficNetwork instance.
* @param peerId Originating peer ID.
* @param sourceId Calling subscriber ID.
* @param destinationId Called subscriber or talkgroup ID.
* @param unitToUnit Whether the request is an individual call.
* @return True when the request is accepted.
*/
static bool processDMRGrantRequest(network::TrafficNetwork& network, uint32_t peerId,
uint32_t sourceId, uint32_t destinationId, bool unitToUnit);
/**
* @brief Processes a synthetic NXDN FNE grant request.
* @param network The TrafficNetwork instance.
* @param peerId Originating peer ID.
* @param sourceId Calling subscriber ID.
* @param destinationId Called subscriber or talkgroup ID.
* @param unitToUnit Whether the request is an individual call.
* @return True when the request is accepted.
*/
static bool processNXDNGrantRequest(network::TrafficNetwork& network, uint32_t peerId,
uint32_t sourceId, uint32_t destinationId, bool unitToUnit);
/**
* @brief Sets whether unit-to-unit traffic is globally disabled.
* @param network The TrafficNetwork instance.

@ -200,6 +200,50 @@ bool HostTestHooks::dmrIsGranted(const dmr::Slot& slot, uint32_t dstId)
return slot.s_affiliations->isGranted(dstId);
}
/* Tests whether a DMR grant is classified as group traffic. */
bool HostTestHooks::dmrIsGroupGrant(const dmr::Slot& slot, uint32_t dstId)
{
return slot.s_affiliations->isGroup(dstId);
}
/* Registers a DMR subscriber on the local Tier III site. */
void HostTestHooks::dmrRegisterUnit(dmr::Slot& slot, uint32_t unitId)
{
slot.s_affiliations->unitReg(unitId);
}
/* Starts a synthetic DMR Tier III OACSU transaction. */
bool HostTestHooks::dmrBeginU2UOACSU(dmr::Slot& slot, uint32_t callerId,
uint32_t calledId, uint8_t serviceOptions)
{
return slot.beginU2UOACSU(callerId, calledId, serviceOptions);
}
/* Returns the number of pending DMR OACSU transactions. */
size_t HostTestHooks::dmrPendingU2UOACSU(const dmr::Slot& slot)
{
return slot.m_u2uOACSUTransactions.size();
}
/* Delivers a synthetic called-unit OACSU response. */
bool HostTestHooks::dmrRespondU2UOACSU(dmr::Slot& slot, uint32_t calledId,
uint32_t callerId, bool accepted)
{
return slot.processU2UOACSUResponse(calledId, callerId, accepted);
}
/* Cancels a synthetic pending DMR OACSU transaction. */
bool HostTestHooks::dmrCancelU2UOACSU(dmr::Slot& slot, uint32_t callerId)
{
return slot.cancelU2UOACSU(callerId);
}
/* Gets a P25 Phase 2 slot for tests. */
p25::phase2::Slot& HostTestHooks::p25P2Slot(p25::phase2::Control& control, uint32_t slotNo)

@ -140,6 +140,52 @@ public:
* @brief Tests whether the shared DMR affiliation table contains a grant.
*/
static bool dmrIsGranted(const dmr::Slot& slot, uint32_t dstId);
/**
* @brief Tests whether a DMR grant is classified as group traffic.
* @param slot DMR control-channel slot.
* @param dstId Granted destination ID.
* @return True when the grant is a group grant.
*/
static bool dmrIsGroupGrant(const dmr::Slot& slot, uint32_t dstId);
/**
* @brief Registers a DMR subscriber on the local Tier III site.
* @param slot DMR control-channel slot.
* @param unitId Subscriber ID to register.
*/
static void dmrRegisterUnit(dmr::Slot& slot, uint32_t unitId);
/**
* @brief Starts a synthetic DMR Tier III OACSU transaction.
* @param slot DMR control-channel slot.
* @param callerId Calling subscriber ID.
* @param calledId Called subscriber ID.
* @param serviceOptions Voice service options.
* @return True when the transaction is accepted.
*/
static bool dmrBeginU2UOACSU(dmr::Slot& slot, uint32_t callerId,
uint32_t calledId, uint8_t serviceOptions = 0U);
/**
* @brief Returns the number of pending DMR OACSU transactions.
* @param slot DMR control-channel slot.
* @return Number of pending transactions.
*/
static size_t dmrPendingU2UOACSU(const dmr::Slot& slot);
/**
* @brief Delivers a synthetic called-unit OACSU response.
* @param slot DMR control-channel slot.
* @param calledId Called subscriber ID.
* @param callerId Calling subscriber ID.
* @param accepted Whether the called unit accepted the request.
* @return True when a matching transaction was processed.
*/
static bool dmrRespondU2UOACSU(dmr::Slot& slot, uint32_t calledId,
uint32_t callerId, bool accepted);
/**
* @brief Cancels a synthetic pending DMR OACSU transaction.
* @param slot DMR control-channel slot.
* @param callerId Calling subscriber ID.
* @return True when a transaction was cancelled.
*/
static bool dmrCancelU2UOACSU(dmr::Slot& slot, uint32_t callerId);
/** @} */
/**

@ -12,6 +12,8 @@
#include <cstring>
#include "common/nxdn/lc/rcch/MESSAGE_TYPE_IDLE.h"
#include "common/nxdn/lc/rcch/MESSAGE_TYPE_VCALL_ASSGN.h"
#include "common/nxdn/lc/rcch/MESSAGE_TYPE_VCALL_CONN.h"
#include "common/nxdn/lc/rcch/RCCHFactory.h"
#include "common/nxdn/NXDNDefines.h"
@ -35,6 +37,44 @@ TEST_CASE("RCCH IDLE encode/decode preserves message type", "[nxdn][rcch]") {
REQUIRE(decoded.toString() == "IDLE (Idle)");
}
TEST_CASE("RCCH voice response decodes its on-air cause", "[nxdn][rcch][u2u]")
{
uint8_t data[NXDN_RCCH_LC_LENGTH_BYTES + 2U] = {};
MESSAGE_TYPE_VCALL_CONN response;
response.setMessageType(MessageType::RCCH_VCALL_REC);
response.setCallType(CallType::INDIVIDUAL);
response.setSrcId(1002U);
response.setDstId(1001U);
response.setCauseResponse(CauseResponse::VD_QUE_TGT_UNIT_BUSY);
response.encode(data, NXDN_RCCH_LC_LENGTH_BITS);
REQUIRE(data[7U] == CauseResponse::VD_QUE_TGT_UNIT_BUSY);
std::unique_ptr<RCCH> decoded = RCCHFactory::createRCCH(
data, NXDN_RCCH_LC_LENGTH_BITS);
REQUIRE(decoded != nullptr);
CHECK(decoded->getCauseResponse() == CauseResponse::VD_QUE_TGT_UNIT_BUSY);
}
TEST_CASE("RCCH voice assignment uses all ten channel bits", "[nxdn][rcch][grant]")
{
for (const uint16_t channel : { 255U, 256U, 511U, 512U, 1023U }) {
uint8_t data[NXDN_RCCH_LC_LENGTH_BYTES + 2U] = {};
MESSAGE_TYPE_VCALL_ASSGN assignment;
assignment.setCallType(CallType::INDIVIDUAL);
assignment.setSrcId(1001U);
assignment.setDstId(1002U);
assignment.setGrpVchNo(channel);
assignment.encode(data, NXDN_RCCH_LC_LENGTH_BITS);
CHECK((data[7U] & 0x03U) == ((channel >> 8U) & 0x03U));
CHECK(data[8U] == (channel & 0xFFU));
MESSAGE_TYPE_VCALL_ASSGN decoded;
decoded.decode(data, NXDN_RCCH_LC_LENGTH_BITS);
CHECK(decoded.getGrpVchNo() == channel);
}
}
TEST_CASE("RCCHFactory creates IDLE instance from encoded buffer", "[nxdn][rcch]") {
uint8_t data[NXDN_RCCH_LC_LENGTH_BYTES + 2U];
::memset(data, 0x00U, sizeof(data));
@ -47,3 +87,25 @@ TEST_CASE("RCCHFactory creates IDLE instance from encoded buffer", "[nxdn][rcch]
REQUIRE(rcch->getMessageType() == MessageType::IDLE);
REQUIRE(rcch->toString() == "IDLE (Idle)");
}
TEST_CASE("RCCH voice call request preserves individual and group classification", "[nxdn][rcch][u2u]")
{
for (const auto callType : { CallType::INDIVIDUAL, CallType::CONFERENCE }) {
uint8_t data[NXDN_RCCH_LC_LENGTH_BYTES + 2U] = {};
MESSAGE_TYPE_VCALL_CONN request;
request.setMessageType(MessageType::RTCH_VCALL);
request.setCallType(callType);
request.setSrcId(1001U);
request.setDstId(2001U);
request.encode(data, NXDN_RCCH_LC_LENGTH_BITS);
std::unique_ptr<RCCH> decoded = RCCHFactory::createRCCH(
data, NXDN_RCCH_LC_LENGTH_BITS);
REQUIRE(decoded != nullptr);
CHECK(decoded->getCallType() == callType);
CHECK(decoded->getGroup() == (callType != CallType::INDIVIDUAL));
CHECK(decoded->getSrcId() == 1001U);
CHECK(decoded->getDstId() == 2001U);
}
}

@ -281,3 +281,24 @@ TEST_CASE("RTCH derives encrypted state from VCALL cipher type", "[nxdn][rtch][s
REQUIRE(decoded.getAlgId() == 0x02U);
REQUIRE(decoded.getKId() == 0x15U);
}
TEST_CASE("RTCH TX_REL_EX preserves transmission permission", "[nxdn][rtch][u2u]") {
for (const bool permit : { false, true }) {
RTCH release;
release.setMessageType(MessageType::RTCH_TX_REL_EX);
release.setSrcId(1001U);
release.setDstId(1002U);
release.setTransmissionPermit(permit);
uint8_t data[NXDN_RTCH_LC_LENGTH_BYTES] = {};
release.encode(data, NXDN_FACCH1_LENGTH_BITS);
REQUIRE((data[7U] & 0x80U) == (permit ? 0x80U : 0x00U));
RTCH decoded;
decoded.decode(data, NXDN_FACCH1_LENGTH_BITS);
CHECK(decoded.getMessageType() == MessageType::RTCH_TX_REL_EX);
CHECK(decoded.getSrcId() == 1001U);
CHECK(decoded.getDstId() == 1002U);
CHECK(decoded.getTransmissionPermit() == permit);
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.