correct Tier III call teardown and slot allocation;

pull/133/head
Bryan Biedenkapp 1 month ago
parent 35a2716c07
commit 25dfc17d44

@ -168,6 +168,7 @@ namespace dmr
const uint32_t WUID_SUPLI = 0xFFFEC4U; //!< Supplementary Data Service Working Unit ID
const uint32_t WUID_SDMI = 0xFFFEC5U; //!< UDT Short Data Service Working Unit ID
const uint32_t WUID_REGI = 0xFFFEC6U; //!< Registration Working Unit ID
const uint32_t WUID_TSI = 0xFFFECAU; //!< Trunking Station Identifier
const uint32_t WUID_STUNI = 0xFFFECCU; //!< MS Stun/Revive Identifier
const uint32_t WUID_AUTHI = 0xFFFECDU; //!< Authentication Working Unit ID
const uint32_t WUID_KILLI = 0xFFFECFU; //!< MS Kill Identifier

@ -45,7 +45,7 @@ bool CSBK_MAINT::decode(const uint8_t* data)
ulong64_t csbkValue = CSBK::toValue(csbk);
m_maintKind = (uint8_t)(((csbkValue >> 48) & 0xFFU) >> 1); // Maintainence Kind
m_dstId = (uint32_t)((csbkValue >> 24) & 0xFFFFU); // Target Radio Address
m_dstId = (uint32_t)((csbkValue >> 24) & 0xFFFFFFU); // Target Radio Address
m_srcId = (uint32_t)(csbkValue & 0xFFFFFFU); // Source Radio Address
return true;

@ -34,7 +34,19 @@ bool CSBK_P_CLEAR::decode(const uint8_t* data)
{
assert(data != nullptr);
/* stub */
uint8_t csbk[DMR_CSBK_LENGTH_BYTES];
::memset(csbk, 0x00U, DMR_CSBK_LENGTH_BYTES);
bool ret = CSBK::decode(data, csbk);
if (!ret)
return false;
ulong64_t csbkValue = CSBK::toValue(csbk);
m_logicalCh1 = (uint32_t)((csbkValue >> 52) & 0xFFFU); // Logical Physical Channel
m_GI = ((csbkValue >> 48) & 0x01U) == 0x01U; // Group/Individual Flag
m_dstId = (uint32_t)((csbkValue >> 24) & 0xFFFFFFU); // Target Address
m_srcId = (uint32_t)(csbkValue & 0xFFFFFFU); // Trunking Station Identifier
return true;
}

@ -161,6 +161,8 @@ Slot::Slot(uint32_t slotNo, uint32_t timeout, uint32_t tgHang, uint32_t queueSiz
m_tsccPayloadGroup(false),
m_tsccPayloadVoice(true),
m_tsccPayloadActRetry(1000U, 0U, 250U),
m_tsccPayloadActTimeout(1000U, 15U),
m_tsccPayloadActRetryCount(0U),
m_tsccAdjSSCnt(0U),
m_disableGrantSrcIdCheck(false),
m_lastLateEntry(0U),
@ -664,6 +666,24 @@ void Slot::clock()
// activate payload channel if requested from the TSCC
if (s_dmr->m_tsccPayloadActive) {
if (m_tsccPayloadActTimeout.isRunning()) {
m_tsccPayloadActTimeout.clock(ms);
if (m_tsccPayloadActTimeout.hasExpired()) {
uint32_t dstId = m_tsccPayloadDstId;
LogWarning(LOG_DMR, "DMR Slot %u, payload activation expired after %u retries, dstId = %u",
m_slotNo, m_tsccPayloadActRetryCount, dstId);
Slot* tscc = s_dmr->getTSCCSlot();
if (tscc != nullptr && tscc->m_enableTSCC && tscc->s_affiliations->isGranted(dstId)) {
tscc->s_affiliations->releaseGrant(dstId, false);
}
else {
notifyCC_ReleaseGrant(dstId);
s_dmr->tsccClearActivatedSlot(m_slotNo);
}
}
}
if (m_rfState == RS_RF_LISTENING && m_netState == RS_NET_IDLE) {
if (m_tsccPayloadDstId > 0U) {
if (m_tsccPayloadActRetry.isRunning()) {
@ -671,6 +691,7 @@ void Slot::clock()
if (m_tsccPayloadActRetry.hasExpired()) {
m_control->writeRF_CSBK_Payload_Activate(m_tsccPayloadDstId, m_tsccPayloadSrcId, m_tsccPayloadGroup, m_tsccPayloadVoice, true);
m_tsccPayloadActRetryCount++;
m_tsccPayloadActRetry.start(0U, 500U);
}
}
@ -999,7 +1020,9 @@ void Slot::setTSCCActivated(uint32_t dstId, uint32_t srcId, bool group, bool voi
}
if (m_tsccPayloadDstId != 0U && !m_tsccPayloadActRetry.isRunning()) {
m_tsccPayloadActRetryCount = 0U;
m_tsccPayloadActRetry.start();
m_tsccPayloadActTimeout.start();
}
}
@ -1275,7 +1298,7 @@ void Slot::processFrameLoss(RPT_RF_LOSS_TYPE type)
tscc->s_affiliations->releaseGrant(m_rfLC->getDstId(), false);
}
clearTSCCActivated();
s_dmr->tsccClearActivatedSlot(m_slotNo);
if (!tscc->m_enableTSCC) {
notifyCC_ReleaseGrant(m_rfLC->getDstId());
@ -1534,7 +1557,7 @@ void Slot::writeEndNet(bool writeEnd)
tscc->s_affiliations->releaseGrant(m_netLC->getDstId(), false);
}
clearTSCCActivated();
s_dmr->tsccClearActivatedSlot(m_slotNo);
if (!tscc->m_enableTSCC) {
notifyCC_ReleaseGrant(m_netLC->getDstId());
@ -1706,6 +1729,17 @@ void Slot::clearTSCCActivated()
m_tsccPayloadVoice = true;
m_tsccPayloadActRetry.stop();
m_tsccPayloadActTimeout.stop();
m_tsccPayloadActRetryCount = 0U;
}
/* Stops payload activation timers after traffic has arrived. */
void Slot::confirmTSCCPayloadActive()
{
m_tsccPayloadActRetry.stop();
m_tsccPayloadActTimeout.stop();
m_tsccPayloadActRetryCount = 0U;
}
/* Helper to set the DMR short LC. */

@ -443,6 +443,8 @@ namespace dmr
bool m_tsccPayloadGroup;
bool m_tsccPayloadVoice;
Timer m_tsccPayloadActRetry;
Timer m_tsccPayloadActTimeout;
uint8_t m_tsccPayloadActRetryCount;
uint8_t m_tsccAdjSSCnt;
bool m_disableGrantSrcIdCheck;
@ -586,6 +588,10 @@ namespace dmr
* @brief Clears the flag indicating whether the slot is a TSCC payload slot.
*/
void clearTSCCActivated();
/**
* @brief Stops payload activation retry/deadline timers after traffic begins.
*/
void confirmTSCCPayloadActive();
/**
* @brief Helper to set the DMR short LC.

@ -48,10 +48,6 @@ bool DMRAffiliationLookup::grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t s
return false;
}
if (!m_chLookup->isRFChAvailable()) {
return false;
}
uint32_t chNo = getAvailableChannelForSlot(slot);
if (chNo == 0U) {
return false;
@ -61,8 +57,16 @@ bool DMRAffiliationLookup::grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t s
return false;
}
if (getAvailableSlotForChannel(chNo) == 0U || chNo == m_tsccChNo) {
m_chLookup->allocRFCh(chNo);
bool channelAlreadyManaged = chNo == m_tsccChNo;
for (auto entry : m_grantChSlotTable) {
if (std::get<0>(entry.second) == chNo) {
channelAlreadyManaged = true;
break;
}
}
if (!channelAlreadyManaged && !m_chLookup->allocRFCh(chNo)) {
return false;
}
__lock();
@ -135,8 +139,18 @@ bool DMRAffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll)
m_grantSrcIdTable.erase(dstId);
m_grantChSlotTable.erase(dstId);
m_netGrantedTable.erase(dstId);
m_uuGrantedTable.erase(dstId);
m_chLookup->freeRFCh(chNo);
bool channelStillGranted = false;
for (auto entry : m_grantChSlotTable) {
if (std::get<0>(entry.second) == chNo) {
channelStillGranted = true;
break;
}
}
if (!channelStillGranted && chNo != m_tsccChNo) {
m_chLookup->freeRFCh(chNo);
}
if (m_rfGrantChCnt > 0U) {
m_rfGrantChCnt--;
@ -146,6 +160,7 @@ bool DMRAffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll)
}
m_grantTimers[dstId].stop();
m_grantTimers.erase(dstId);
__unlock();
@ -165,29 +180,19 @@ bool DMRAffiliationLookup::isChBusy(uint32_t chNo) const
__spinlock();
// lookup dynamic channel grant table entry
for (auto grantEntry : m_grantChTable) {
if (grantEntry.second == chNo) {
uint8_t slotCount = 0U;
if (chNo == m_tsccChNo) {
slotCount++; // one slot is *always* used for TSCC
}
for (auto slotEntry : m_grantChSlotTable) {
uint32_t foundChNo = std::get<0>(slotEntry.second);
if (foundChNo == chNo)
slotCount++;
}
bool slot1Busy = chNo == m_tsccChNo && m_tsccSlot == 1U;
bool slot2Busy = chNo == m_tsccChNo && m_tsccSlot == 2U;
for (auto entry : m_grantChSlotTable) {
if (std::get<0>(entry.second) != chNo)
continue;
if (slotCount == 2U) {
return true;
} else {
return false;
}
}
if (std::get<1>(entry.second) == 1U)
slot1Busy = true;
if (std::get<1>(entry.second) == 2U)
slot2Busy = true;
}
return false;
return slot1Busy && slot2Busy;
}
/* Helper to get the slot granted for the given destination ID. */
@ -222,6 +227,10 @@ void DMRAffiliationLookup::setSlotForChannelTSCC(uint32_t chNo, uint8_t slot)
m_tsccChNo = chNo;
m_tsccSlot = slot;
// Reserve the physical control channel in the shared allocator. DMR tracks
// the opposing payload timeslot separately in m_grantChSlotTable.
m_chLookup->allocRFCh(chNo);
}
/* Helper to determine the an available channel for a slot. */
@ -234,36 +243,40 @@ uint32_t DMRAffiliationLookup::getAvailableChannelForSlot(uint8_t slot) const
__spinlock();
uint32_t chNo = 0U;
for (auto entry : m_chLookup->rfChDataTable()) {
if (entry.second.chNo() == m_tsccChNo && slot == m_tsccSlot) {
continue;
} else {
// lookup dynamic channel slot grant table entry
bool chAvailSlot = false;
for (auto gntEntry : m_grantChSlotTable) {
uint32_t foundChNo = std::get<0>(gntEntry.second);
if (foundChNo == entry.second.chNo()) {
uint8_t foundSlot = std::get<1>(gntEntry.second);
if (slot == foundSlot)
continue;
else {
chAvailSlot = true;
break;
}
}
// Prefer a physical channel already managed by DMR so its unused timeslot
// can be filled without changing the shared ChannelLookup allocation model.
for (auto grant : m_grantChSlotTable) {
uint32_t chNo = std::get<0>(grant.second);
bool requestedSlotBusy = false;
for (auto other : m_grantChSlotTable) {
if (std::get<0>(other.second) == chNo && std::get<1>(other.second) == slot) {
requestedSlotBusy = true;
break;
}
}
// if we have no granted channels -- return true
if (m_grantChSlotTable.size() == 0U)
chAvailSlot = true;
if (!requestedSlotBusy && !(chNo == m_tsccChNo && slot == m_tsccSlot))
return chNo;
}
chNo = entry.second.chNo();
if (chAvailSlot)
// The timeslot opposing the TSCC is also a valid payload resource.
if (m_tsccChNo != 0U && slot != m_tsccSlot) {
bool requestedSlotBusy = false;
for (auto grant : m_grantChSlotTable) {
if (std::get<0>(grant.second) == m_tsccChNo && std::get<1>(grant.second) == slot) {
requestedSlotBusy = true;
break;
}
}
if (!requestedSlotBusy)
return m_tsccChNo;
}
// For a new physical channel, preserve ChannelLookup's allocation state so
// P25 and NXDN users of that shared class are unaffected.
uint32_t chNo = m_chLookup->getFirstRFChannel();
if (chNo == m_tsccChNo && slot == m_tsccSlot)
return 0U;
return chNo;
}

@ -335,6 +335,32 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len)
LogInfoEx(LOG_RF, "DMR Slot %u, CSBK, %s, kind = $%02X, srcId = %u",
m_slot->m_slotNo, csbk->toString().c_str(), isp->getMaintKind(), srcId);
}
// ETSI TS 102 361-4 6.6.2.3.1.6: an applicable inbound
// P_MAINT(DISCON) terminates use of the payload channel and the TS
// responds with P_CLEAR. Other maintenance kinds are reserved.
bool applicableSource = srcId == m_slot->m_tsccPayloadSrcId ||
(!m_slot->m_tsccPayloadGroup && srcId == m_slot->m_tsccPayloadDstId);
if (isp->getMaintKind() == 0U && dstId == WUID_TSI &&
m_slot->m_tsccPayloadDstId != 0U && applicableSource) {
uint32_t payloadDstId = m_slot->m_tsccPayloadDstId;
if (m_slot->m_rfState == RS_RF_AUDIO || m_slot->m_rfState == RS_RF_DATA) {
m_slot->processFrameLoss(RF_LOSS_TYPE_IN_CALL_CONTROL);
}
Slot* tscc = m_slot->s_dmr->getTSCCSlot();
if (tscc != nullptr && tscc->m_enableTSCC && tscc->s_affiliations->isGranted(payloadDstId)) {
tscc->s_affiliations->releaseGrant(payloadDstId, false);
}
else if (m_slot->m_tsccPayloadDstId != 0U) {
m_slot->notifyCC_ReleaseGrant(payloadDstId);
}
m_slot->s_dmr->tsccClearActivatedSlot(m_slot->m_slotNo);
handled = true;
}
}
break;
case CSBKO::PRECCSBK:
@ -899,7 +925,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
uint32_t availChNo = tscc->s_affiliations->getAvailableChannelForSlot(slot);
if (!tscc->s_affiliations->rfCh()->isRFChAvailable() || availChNo == 0U) {
if (availChNo == 0U) {
if (grp) {
if (!net) {
LogWarning(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access, GRP_VOICE_CALL (Group Voice Call) queued, no channels available, dstId = %u", tscc->m_slotNo, dstId);
@ -929,6 +955,9 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_
slot = tscc->s_affiliations->getGrantedSlot(dstId);
//tscc->s_siteData.setChCnt(tscc->s_affiliations->getRFChCnt() + tscc->s_affiliations->getGrantedRFChCnt());
}
else {
return false;
}
}
}
else {
@ -1219,7 +1248,7 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u
slot = groupVoice.source().tgSlot();
uint32_t availChNo = tscc->s_affiliations->getAvailableChannelForSlot(slot);
if (!tscc->s_affiliations->rfCh()->isRFChAvailable() || availChNo == 0U) {
if (availChNo == 0U) {
if (grp) {
if (!net) {
LogWarning(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access, GRP_DATA_CALL (Group Data Call) queued, no channels available, dstId = %u", tscc->m_slotNo, dstId);
@ -1250,6 +1279,9 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u
//tscc->m_siteData.setChCnt(tscc->m_affiliations->getRFChCnt() + tscc->m_affiliations->getGrantedRFChCnt());
}
else {
return false;
}
}
}
else {
@ -1522,15 +1554,16 @@ void ControlSignaling::writeRF_CSBK_Payload_Clear(uint32_t dstId, uint32_t srcId
csbk->setLastBlock(true);
csbk->setLogicalCh1(m_slot->s_channelNo);
csbk->setSlotNo(m_slot->m_slotNo);
// Zero directs the MS back to its last-confirmed TSCC and is valid for
// both co-located and distributed payload-channel hosts.
csbk->setLogicalCh1(0U);
csbk->setSrcId(srcId);
csbk->setSrcId(WUID_TSI);
csbk->setDstId(dstId);
if (m_verbose) {
LogInfoEx(LOG_RF, "DMR Slot %u, CSBK, %s, group = %u, chNo = %u, slot = %u, srcId = %u, dstId = %u",
m_slot->m_slotNo, csbk->toString().c_str(), csbk->getGI(), csbk->getLogicalCh1(), csbk->getSlotNo(), srcId, dstId);
m_slot->m_slotNo, csbk->toString().c_str(), csbk->getGI(), csbk->getLogicalCh1(), csbk->getSlotNo(), csbk->getSrcId(), dstId);
}
for (uint8_t i = 0; i < 2U; i++)

@ -147,7 +147,7 @@ bool Data::process(uint8_t* data, uint32_t len)
}
if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) {
m_slot->m_tsccPayloadActRetry.stop();
m_slot->confirmTSCCPayloadActive();
}
// validate the source RID
@ -426,7 +426,7 @@ void Data::processNetwork(const data::NetData& dmrData)
}
if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) {
m_slot->m_tsccPayloadActRetry.stop();
m_slot->confirmTSCCPayloadActive();
}
m_netDataBlockCnt = 0U;

@ -87,7 +87,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
return false;
if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) {
m_slot->m_tsccPayloadActRetry.stop();
m_slot->confirmTSCCPayloadActive();
}
// validate source RID
@ -765,7 +765,7 @@ void Voice::processNetwork(const data::NetData& dmrData)
return;
if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) {
m_slot->m_tsccPayloadActRetry.stop();
m_slot->confirmTSCCPayloadActive();
}
if (dstId != dmrData.getDstId() || srcId != dmrData.getSrcId() || flco != dmrData.getFLCO())

@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Test Suite
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "host/Defines.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/lc/csbk/CSBK_MAINT.h"
#include "common/dmr/lc/csbk/CSBK_P_CLEAR.h"
#include "common/lookups/ChannelLookup.h"
#include "host/dmr/lookups/DMRAffiliationLookup.h"
#include <catch2/catch_test_macros.hpp>
using namespace dmr::defines;
using namespace dmr::lc::csbk;
TEST_CASE("DMR Tier III maintenance preserves 24-bit addresses", "[dmr][tier3][csbk]")
{
CSBK_MAINT encoded;
encoded.setLastBlock(true);
encoded.setMaintKind(0U);
encoded.setDstId(WUID_TSI);
encoded.setSrcId(0xABCDEFU);
uint8_t burst[DMR_FRAME_LENGTH_BYTES] = { 0U };
encoded.encode(burst);
CSBK_MAINT decoded;
decoded.setDataType(DataType::CSBK);
REQUIRE(decoded.decode(burst));
CHECK(decoded.getMaintKind() == 0U);
CHECK(decoded.getDstId() == WUID_TSI);
CHECK(decoded.getSrcId() == 0xABCDEFU);
}
TEST_CASE("DMR Tier III payload clear round trip carries TSCC return fields", "[dmr][tier3][csbk]")
{
CSBK_P_CLEAR encoded;
encoded.setLastBlock(true);
encoded.setLogicalCh1(0U); // return to last-confirmed TSCC
encoded.setGI(true);
encoded.setDstId(0x123456U);
encoded.setSrcId(WUID_TSI);
uint8_t burst[DMR_FRAME_LENGTH_BYTES] = { 0U };
encoded.encode(burst);
CSBK_P_CLEAR decoded;
decoded.setDataType(DataType::CSBK);
REQUIRE(decoded.decode(burst));
CHECK(decoded.getLogicalCh1() == 0U);
CHECK(decoded.getGI());
CHECK(decoded.getDstId() == 0x123456U);
CHECK(decoded.getSrcId() == WUID_TSI);
}
TEST_CASE("DMR Tier III grants account for both slots without changing ChannelLookup", "[dmr][tier3][grant]")
{
::lookups::ChannelLookup channels;
REQUIRE(channels.initializeRFCh(101U));
dmr::lookups::DMRAffiliationLookup affiliations(&channels, false);
REQUIRE(affiliations.grantChSlot(2001U, 1001U, 1U, 15U, true, false));
CHECK_FALSE(channels.isRFChAvailable());
CHECK_FALSE(affiliations.isChBusy(101U));
// The physical channel bit is already allocated, but DMR slot 2 remains usable.
REQUIRE(affiliations.grantChSlot(2002U, 1002U, 2U, 15U, true, false));
CHECK(affiliations.getGrantedCh(2002U) == 101U);
CHECK(affiliations.isChBusy(101U));
// Releasing one logical slot must not free the shared physical carrier.
REQUIRE(affiliations.releaseGrant(2001U, false));
CHECK_FALSE(channels.isRFChAvailable());
CHECK_FALSE(affiliations.isChBusy(101U));
REQUIRE(affiliations.releaseGrant(2002U, false));
CHECK(channels.isRFChAvailable());
}
TEST_CASE("DMR Tier III does not double-grant one slot", "[dmr][tier3][grant]")
{
::lookups::ChannelLookup channels;
REQUIRE(channels.initializeRFCh(101U));
dmr::lookups::DMRAffiliationLookup affiliations(&channels, false);
REQUIRE(affiliations.grantChSlot(2001U, 1001U, 1U, 15U, true, false));
CHECK_FALSE(affiliations.grantChSlot(2002U, 1002U, 1U, 15U, true, false));
}
Loading…
Cancel
Save

Powered by TurnKey Linux.