From 25dfc17d44fe745fa5f8ee68c383bf05f7e648d2 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sat, 10 Oct 2026 10:02:36 -0400 Subject: [PATCH] correct Tier III call teardown and slot allocation; --- src/common/dmr/DMRDefines.h | 1 + src/common/dmr/lc/csbk/CSBK_MAINT.cpp | 2 +- src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp | 14 ++- src/host/dmr/Slot.cpp | 38 +++++- src/host/dmr/Slot.h | 6 + src/host/dmr/lookups/DMRAffiliationLookup.cpp | 113 ++++++++++-------- src/host/dmr/packet/ControlSignaling.cpp | 45 ++++++- src/host/dmr/packet/Data.cpp | 4 +- src/host/dmr/packet/Voice.cpp | 4 +- tests/dmr/Tier3Lifecycle_Tests.cpp | 96 +++++++++++++++ 10 files changed, 259 insertions(+), 64 deletions(-) create mode 100644 tests/dmr/Tier3Lifecycle_Tests.cpp diff --git a/src/common/dmr/DMRDefines.h b/src/common/dmr/DMRDefines.h index 90c49116..bed6ffeb 100644 --- a/src/common/dmr/DMRDefines.h +++ b/src/common/dmr/DMRDefines.h @@ -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 diff --git a/src/common/dmr/lc/csbk/CSBK_MAINT.cpp b/src/common/dmr/lc/csbk/CSBK_MAINT.cpp index 56ff82ce..950a21fb 100644 --- a/src/common/dmr/lc/csbk/CSBK_MAINT.cpp +++ b/src/common/dmr/lc/csbk/CSBK_MAINT.cpp @@ -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; diff --git a/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp b/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp index 8b9644b5..4088575e 100644 --- a/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp +++ b/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp @@ -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; } diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index c44b6a12..5fc25238 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -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. */ diff --git a/src/host/dmr/Slot.h b/src/host/dmr/Slot.h index 27ec24e0..6470b92f 100644 --- a/src/host/dmr/Slot.h +++ b/src/host/dmr/Slot.h @@ -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. diff --git a/src/host/dmr/lookups/DMRAffiliationLookup.cpp b/src/host/dmr/lookups/DMRAffiliationLookup.cpp index 34099a2b..51b1e9e5 100644 --- a/src/host/dmr/lookups/DMRAffiliationLookup.cpp +++ b/src/host/dmr/lookups/DMRAffiliationLookup.cpp @@ -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; } diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index 1c8ab9c1..b740f9e4 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -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++) diff --git a/src/host/dmr/packet/Data.cpp b/src/host/dmr/packet/Data.cpp index 6d602d55..b3d615ec 100644 --- a/src/host/dmr/packet/Data.cpp +++ b/src/host/dmr/packet/Data.cpp @@ -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; diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index 02337436..e6a5543e 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -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()) diff --git a/tests/dmr/Tier3Lifecycle_Tests.cpp b/tests/dmr/Tier3Lifecycle_Tests.cpp new file mode 100644 index 00000000..cc03e9e6 --- /dev/null +++ b/tests/dmr/Tier3Lifecycle_Tests.cpp @@ -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 + +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)); +}