From 33b9e25f551d6636e1820f6301f88107e8b46b5f Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sat, 10 Oct 2026 10:42:43 -0400 Subject: [PATCH] recover Tier III grants for payload-channel re-key; --- src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp | 1 - src/host/dmr/Control.cpp | 18 ++ src/host/dmr/Slot.cpp | 54 ++++++ src/host/dmr/Slot.h | 9 + src/host/dmr/lookups/DMRAffiliationLookup.cpp | 73 +++------ src/host/dmr/lookups/DMRAffiliationLookup.h | 2 +- src/host/dmr/packet/ControlSignaling.cpp | 8 +- src/host/dmr/packet/ControlSignaling.h | 3 +- src/host/dmr/packet/Data.cpp | 8 +- src/host/dmr/packet/Voice.cpp | 6 + tests/dmr/HostControl_Tests.cpp | 35 +++- tests/dmr/Tier3Lifecycle_Tests.cpp | 155 ++++++++++++++++++ tests/host/HostTestHooks.cpp | 23 +++ tests/host/HostTestHooks.h | 12 ++ 14 files changed, 350 insertions(+), 57 deletions(-) diff --git a/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp b/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp index 4088575e..4e97dd74 100644 --- a/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp +++ b/src/common/dmr/lc/csbk/CSBK_P_CLEAR.cpp @@ -60,7 +60,6 @@ void CSBK_P_CLEAR::encode(uint8_t* data) ulong64_t csbkValue = 0U; csbkValue = (csbkValue << 12) + (m_logicalCh1 & 0xFFFU); // Logical Physical Channel 1 - csbkValue = (csbkValue << 1) + 0U; // Reserved csbkValue = (csbkValue << 3) + 0U; // Reserved csbkValue = (csbkValue << 1) + ((m_GI) ? 1U : 0U); // Group/Individual Flag csbkValue = (csbkValue << 24) + m_dstId; // Talkgroup ID diff --git a/src/host/dmr/Control.cpp b/src/host/dmr/Control.cpp index 6e480a5c..b886cc22 100644 --- a/src/host/dmr/Control.cpp +++ b/src/host/dmr/Control.cpp @@ -924,6 +924,24 @@ void Control::RPC_touchGrantTG(json::object& req, json::object& reply) return; } + Slot* tscc = getTSCCSlot(); + if (tscc != nullptr && tscc->m_enableTSCC && !tscc->s_affiliations->isGranted(dstId) && + req["srcId"].is() && req["group"].is() && req["chNo"].is()) { + uint32_t srcId = req["srcId"].get(); + bool group = req["group"].get(); + uint32_t chNo = req["chNo"].get(); + ::lookups::VoiceChData voiceCh = tscc->s_affiliations->rfCh()->getRFChData(chNo); + if (!voiceCh.isValidCh()) { + g_RPC->defaultResponse(reply, "DMR payload channel is not configured", network::NetRPC::INVALID_ARGS); + return; + } + if (!tscc->m_control->writeRF_CSBK_Grant(srcId, dstId, 4U, group, + true, false, chNo, slot)) { + g_RPC->defaultResponse(reply, "failed to recreate DMR payload grant", network::NetRPC::BAD_REQUEST); + return; + } + } + // LogDebugEx(LOG_DMR, "Control::RPC_touchGrantTG()", "callback, dstId = %u, slot = %u", dstId, slot); switch (slot) { diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 5fc25238..859f140b 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -962,6 +962,56 @@ void Slot::touchGrantTG(uint32_t dstId) } } +/* Reissues a missing grant for traffic beginning on a retained payload channel. */ + +bool Slot::ensureTSCCPayloadGrant(uint32_t srcId, uint32_t dstId, bool grp) +{ + Slot* tscc = s_dmr->getTSCCSlot(); + if (tscc == this) + return true; + + // A normal RAND/grant activation already identifies this payload call. + // Only synthesize the grant when the subscriber re-keys directly on the + // retained traffic channel without returning to the TSCC. + if (m_tsccPayloadDstId != 0U) + return true; + + bool granted = false; + if (tscc != nullptr && tscc->m_enableTSCC) { + granted = tscc->m_control->writeRF_CSBK_Grant(srcId, dstId, 4U, grp, + true, false, s_channelNo, (uint8_t)m_slotNo); + } + else if (m_notifyCC && !s_controlChData.address().empty() && s_controlChData.port() != 0U) { + json::object req = json::object(); + req["dstId"].set(dstId); + req["srcId"].set(srcId); + uint8_t slot = (uint8_t)m_slotNo; + req["slot"].set(slot); + req["group"].set(grp); + req["chNo"].set(s_channelNo); + + bool rpcAccepted = false; + bool sent = g_RPC->req(RPC_TOUCH_DMR_TG, req, + [=, &rpcAccepted](json::object& req, json::object& reply) { + (void)reply; + rpcAccepted = req["status"].is() && + req["status"].get() == network::NetRPC::OK; + }, s_controlChData.address(), s_controlChData.port(), true); + granted = sent && rpcAccepted; + } + else { + // Conventional/non-trunked operation has no TSCC grant to maintain. + return true; + } + + if (!granted) { + LogWarning(LOG_RF, "DMR Slot %u, unable to reissue missing payload grant, srcId = %u, dstId = %u", + m_slotNo, srcId, dstId); + } + + return granted; +} + /* Clears the current operating RF state back to idle. */ void Slot::clearRFReject() @@ -1304,6 +1354,10 @@ void Slot::processFrameLoss(RPT_RF_LOSS_TYPE type) notifyCC_ReleaseGrant(m_rfLC->getDstId()); } } + else if (m_rfLC != nullptr) { + notifyCC_ReleaseGrant(m_rfLC->getDstId()); + s_dmr->tsccClearActivatedSlot(m_slotNo); + } if (m_rfTimeout) { writeEndRF(); diff --git a/src/host/dmr/Slot.h b/src/host/dmr/Slot.h index 6470b92f..ca270772 100644 --- a/src/host/dmr/Slot.h +++ b/src/host/dmr/Slot.h @@ -215,6 +215,15 @@ namespace dmr * @param dstId Destination ID. */ void touchGrantTG(uint32_t dstId); + /** + * @brief Reissues a missing Tier III grant when a subscriber starts + * traffic directly on a retained payload channel. + * @param srcId Source Radio ID. + * @param dstId Destination ID. + * @param grp Flag indicating group traffic. + * @returns bool True if no grant was needed or a grant was issued. + */ + bool ensureTSCCPayloadGrant(uint32_t srcId, uint32_t dstId, bool grp); /** @} */ /** diff --git a/src/host/dmr/lookups/DMRAffiliationLookup.cpp b/src/host/dmr/lookups/DMRAffiliationLookup.cpp index 51b1e9e5..450e99ff 100644 --- a/src/host/dmr/lookups/DMRAffiliationLookup.cpp +++ b/src/host/dmr/lookups/DMRAffiliationLookup.cpp @@ -42,13 +42,22 @@ bool DMRAffiliationLookup::grantCh(uint32_t dstId, uint32_t srcId, uint32_t gran /* Helper to grant a channel and slot. */ -bool DMRAffiliationLookup::grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t slot, uint32_t grantTimeout, bool grp, bool netGranted) +bool DMRAffiliationLookup::grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t slot, uint32_t grantTimeout, bool grp, bool netGranted, uint32_t chNo) { - if (dstId == 0U) { + if (dstId == 0U || slot == 0U || slot > 2U) { return false; } - uint32_t chNo = getAvailableChannelForSlot(slot); + if (chNo != 0U) { + for (auto entry : m_grantChSlotTable) { + if (std::get<0>(entry.second) == chNo && std::get<1>(entry.second) == slot) + return false; + } + } + else { + chNo = getAvailableChannelForSlot(slot); + } + if (chNo == 0U) { return false; } @@ -290,53 +299,23 @@ uint8_t DMRAffiliationLookup::getAvailableSlotForChannel(uint32_t chNo) const __spinlock(); - uint8_t slot = 1U; - - // lookup dynamic channel slot grant table entry - bool grantedSlot = false; - int slotCount = 0U; + bool slot1Busy = chNo == m_tsccChNo && m_tsccSlot == 1U; + bool slot2Busy = chNo == m_tsccChNo && m_tsccSlot == 2U; for (auto entry : m_grantChSlotTable) { uint32_t foundChNo = std::get<0>(entry.second); - if (foundChNo == chNo) - { - uint8_t foundSlot = std::get<1>(entry.second); - if (slot == foundSlot) { - switch (foundSlot) { - case 1U: - slot = 2U; - break; - case 2U: - slot = 1U; - break; - } - - grantedSlot = true; - slotCount++; - } - } - } - - if (slotCount == 2U) { - slot = 0U; - return slot; - } + if (foundChNo != chNo) + continue; - // are we trying to assign the TSCC slot? - if (chNo == m_tsccChNo && slot == m_tsccSlot) { - if (!grantedSlot) { - // since we didn't find a slot being granted out -- utilize the slot opposing the TSCC - switch (m_tsccSlot) { - case 1U: - slot = 2U; - break; - case 2U: - slot = 1U; - break; - } - } else { - slot = 0U; // TSCC is not assignable - } + uint8_t foundSlot = std::get<1>(entry.second); + if (foundSlot == 1U) + slot1Busy = true; + if (foundSlot == 2U) + slot2Busy = true; } - return slot; + if (!slot1Busy) + return 1U; + if (!slot2Busy) + return 2U; + return 0U; } diff --git a/src/host/dmr/lookups/DMRAffiliationLookup.h b/src/host/dmr/lookups/DMRAffiliationLookup.h index 3a8e475f..3a94d32b 100644 --- a/src/host/dmr/lookups/DMRAffiliationLookup.h +++ b/src/host/dmr/lookups/DMRAffiliationLookup.h @@ -68,7 +68,7 @@ namespace dmr * @param netGranted Flag indicating the grant came from network traffic. * @returns bool True, if the destination address has been granted to the source ID, otherwise false. */ - bool grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t slot, uint32_t grantTimeout, bool grp, bool netGranted); + bool grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t slot, uint32_t grantTimeout, bool grp, bool netGranted, uint32_t chNo = 0U); /** * @brief Helper to release the channel grant for the destination ID. * @param dstId Destination Address. diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index b740f9e4..b7a2c332 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -832,7 +832,7 @@ 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) +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) { Slot* tscc = m_slot->s_dmr->getTSCCSlot(); @@ -887,7 +887,7 @@ 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 = groupVoice.source().tgSlot(); + slot = preferredSlot != 0U ? preferredSlot : groupVoice.source().tgSlot(); if (grp && !tscc->m_ignoreAffiliationCheck) { // is this an affiliation required group? @@ -924,7 +924,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } - uint32_t availChNo = tscc->s_affiliations->getAvailableChannelForSlot(slot); + uint32_t availChNo = chNo != 0U ? chNo : tscc->s_affiliations->getAvailableChannelForSlot(slot); if (availChNo == 0U) { if (grp) { if (!net) { @@ -950,7 +950,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } else { - if (tscc->s_affiliations->grantChSlot(dstId, srcId, slot, GRANT_TIMER_TIMEOUT, grp, net)) { + if (tscc->s_affiliations->grantChSlot(dstId, srcId, slot, GRANT_TIMER_TIMEOUT, grp, net, availChNo)) { chNo = tscc->s_affiliations->getGrantedCh(dstId); slot = tscc->s_affiliations->getGrantedSlot(dstId); //tscc->s_siteData.setChCnt(tscc->s_affiliations->getRFChCnt() + tscc->s_affiliations->getGrantedRFChCnt()); diff --git a/src/host/dmr/packet/ControlSignaling.h b/src/host/dmr/packet/ControlSignaling.h index 64a6806d..1e3e874e 100644 --- a/src/host/dmr/packet/ControlSignaling.h +++ b/src/host/dmr/packet/ControlSignaling.h @@ -156,7 +156,8 @@ namespace dmr * @param skip Flag indicating normal grant checking is skipped. * @param chNo Channel Number. */ - 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); + 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); /** * @brief Helper to write a data grant packet. * @param srcId Source Radio ID. diff --git a/src/host/dmr/packet/Data.cpp b/src/host/dmr/packet/Data.cpp index b3d615ec..f2ba680b 100644 --- a/src/host/dmr/packet/Data.cpp +++ b/src/host/dmr/packet/Data.cpp @@ -83,9 +83,11 @@ bool Data::process(uint8_t* data, uint32_t len) if (m_tscc != nullptr) { if (m_tscc->m_enableTSCC) { m_tscc->s_affiliations->releaseGrant(m_slot->m_rfLC->getDstId(), false); - m_slot->clearTSCCActivated(); } } + else { + m_slot->notifyCC_ReleaseGrant(m_slot->m_rfLC->getDstId()); + } if (m_slot->m_rssi != 0U) { ::ActivityLog("DMR", true, "Slot %u RF end of voice transmission, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", @@ -387,9 +389,11 @@ void Data::processNetwork(const data::NetData& dmrData) if (m_tscc != nullptr) { if (m_tscc->m_enableTSCC) { m_tscc->s_affiliations->releaseGrant(m_slot->m_netLC->getDstId(), false); - m_slot->clearTSCCActivated(); } } + else { + m_slot->notifyCC_ReleaseGrant(m_slot->m_netLC->getDstId()); + } // We've received the voice header and terminator haven't we? m_slot->m_netFrames += 2U; diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index e6a5543e..4c1ce4ad 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -86,6 +86,9 @@ bool Voice::process(uint8_t* data, uint32_t len) if (checkRFTrafficCollision(dstId)) return false; + if (!m_slot->ensureTSCCPayloadGrant(srcId, dstId, flco == FLCO::GROUP)) + return false; + if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) { m_slot->confirmTSCCPayloadActive(); } @@ -587,6 +590,9 @@ bool Voice::process(uint8_t* data, uint32_t len) if (checkRFTrafficCollision(dstId)) return false; + if (!m_slot->ensureTSCCPayloadGrant(srcId, dstId, flco == FLCO::GROUP)) + return false; + // validate the source RID if (!acl::AccessControl::validateSrcId(srcId)) { LogWarning(LOG_RF, "DMR Slot %u, VOICE denial, RID rejection, srcId = %u", m_slot->m_slotNo, srcId); diff --git a/tests/dmr/HostControl_Tests.cpp b/tests/dmr/HostControl_Tests.cpp index 82b8ae26..2000b4a9 100644 --- a/tests/dmr/HostControl_Tests.cpp +++ b/tests/dmr/HostControl_Tests.cpp @@ -775,6 +775,39 @@ TEST_CASE("DMR processFrame accepts a synthetic RF voice call on the targeted sl REQUIRE(harness.m_control->getLastDstId(1U) == 2001U); } +TEST_CASE("DMR Tier III re-key on a retained payload channel recreates its grant", "[dmr][tier3][host][control][rf]") +{ + DMRHostHarness harness; + REQUIRE(harness.m_chLookup.initializeRFCh(101U)); + HostTestHooks::dmrEnableTier3(*harness.m_control, 101U, 1U); + + dmr::Slot* payload = HostTestHooks::dmrSlot2(*harness.m_control); + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*payload, 1001U, 2001U)); + CHECK(HostTestHooks::dmrIsGranted(*payload, 2001U)); + CHECK(HostTestHooks::dmrPayloadDstId(*payload) == 2001U); + + uint8_t terminator[dmr::defines::DMR_FRAME_LENGTH_BYTES + 2U] = { 0U }; + terminator[0U] = modem::TAG_DATA; + terminator[1U] = dmr::defines::SYNC_DATA | dmr::defines::DataType::TERMINATOR_WITH_LC; + buildDMRTerminatorPayload(terminator + 2U, 1001U, 2001U, true); + REQUIRE(payload->processFrame(terminator, sizeof(terminator))); + CHECK_FALSE(HostTestHooks::dmrIsGranted(*payload, 2001U)); + CHECK(HostTestHooks::dmrPayloadDstId(*payload) == 0U); + + // Some subscribers remain on the payload channel during hangtime and + // begin transmitting without another RAND request on the TSCC. + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*payload, 1002U, 2001U)); + CHECK(HostTestHooks::dmrIsGranted(*payload, 2001U)); + CHECK(HostTestHooks::dmrPayloadDstId(*payload) == 2001U); + + terminator[0U] = modem::TAG_DATA; + terminator[1U] = dmr::defines::SYNC_DATA | dmr::defines::DataType::TERMINATOR_WITH_LC; + buildDMRTerminatorPayload(terminator + 2U, 1002U, 2001U, true); + REQUIRE(payload->processFrame(terminator, sizeof(terminator))); + CHECK_FALSE(HostTestHooks::dmrIsGranted(*payload, 2001U)); + CHECK(HostTestHooks::dmrPayloadDstId(*payload) == 0U); +} + TEST_CASE("DMR rfTGHang expiry ends active RF call and returns slot to listening", "[dmr][host][control][rf]") { DMRHostHarness harness; @@ -811,4 +844,4 @@ TEST_CASE("DMR rfTGHang zero fallback arms rfLossWatchdog and recovers RF state" REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_LISTENING); REQUIRE_FALSE(HostTestHooks::dmrRFLossWatchdog(*slot).isRunning()); -} \ No newline at end of file +} diff --git a/tests/dmr/Tier3Lifecycle_Tests.cpp b/tests/dmr/Tier3Lifecycle_Tests.cpp index cc03e9e6..98cf968b 100644 --- a/tests/dmr/Tier3Lifecycle_Tests.cpp +++ b/tests/dmr/Tier3Lifecycle_Tests.cpp @@ -39,6 +39,25 @@ TEST_CASE("DMR Tier III maintenance preserves 24-bit addresses", "[dmr][tier3][c CHECK(decoded.getSrcId() == 0xABCDEFU); } +TEST_CASE("DMR Tier III maintenance round trip preserves field boundaries", "[dmr][tier3][csbk]") +{ + CSBK_MAINT encoded; + encoded.setLastBlock(true); + encoded.setMaintKind(7U); + encoded.setDstId(0xFFFFFFU); + encoded.setSrcId(0x000001U); + + 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() == 7U); + CHECK(decoded.getDstId() == 0xFFFFFFU); + CHECK(decoded.getSrcId() == 0x000001U); +} + TEST_CASE("DMR Tier III payload clear round trip carries TSCC return fields", "[dmr][tier3][csbk]") { CSBK_P_CLEAR encoded; @@ -60,6 +79,27 @@ TEST_CASE("DMR Tier III payload clear round trip carries TSCC return fields", "[ CHECK(decoded.getSrcId() == WUID_TSI); } +TEST_CASE("DMR Tier III individual payload clear preserves maximum channel and addresses", "[dmr][tier3][csbk]") +{ + CSBK_P_CLEAR encoded; + encoded.setLastBlock(true); + encoded.setLogicalCh1(0xFFFU); + encoded.setGI(false); + encoded.setDstId(0xFFFFFFU); + encoded.setSrcId(0x000001U); + + 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() == 0xFFFU); + CHECK_FALSE(decoded.getGI()); + CHECK(decoded.getDstId() == 0xFFFFFFU); + CHECK(decoded.getSrcId() == 0x000001U); +} + TEST_CASE("DMR Tier III grants account for both slots without changing ChannelLookup", "[dmr][tier3][grant]") { ::lookups::ChannelLookup channels; @@ -94,3 +134,118 @@ TEST_CASE("DMR Tier III does not double-grant one slot", "[dmr][tier3][grant]") REQUIRE(affiliations.grantChSlot(2001U, 1001U, 1U, 15U, true, false)); CHECK_FALSE(affiliations.grantChSlot(2002U, 1002U, 1U, 15U, true, false)); } + +TEST_CASE("DMR Tier III rejects grants without a valid destination or RF resource", "[dmr][tier3][grant]") +{ + ::lookups::ChannelLookup channels; + dmr::lookups::DMRAffiliationLookup affiliations(&channels, false); + + CHECK_FALSE(affiliations.grantChSlot(0U, 1001U, 1U, 15U, true, false)); + CHECK_FALSE(affiliations.grantChSlot(2001U, 1001U, 1U, 15U, true, false)); + CHECK(affiliations.getGrantedRFChCnt() == 0U); +} + +TEST_CASE("DMR Tier III spills an occupied slot onto another physical channel", "[dmr][tier3][grant]") +{ + ::lookups::ChannelLookup channels; + REQUIRE(channels.initializeRFCh(101U)); + REQUIRE(channels.initializeRFCh(102U)); + + dmr::lookups::DMRAffiliationLookup affiliations(&channels, false); + REQUIRE(affiliations.grantChSlot(2001U, 1001U, 1U, 15U, true, false)); + REQUIRE(affiliations.grantChSlot(2002U, 1002U, 1U, 15U, true, false)); + + CHECK(affiliations.getGrantedCh(2001U) == 101U); + CHECK(affiliations.getGrantedCh(2002U) == 102U); + CHECK(affiliations.getGrantedSlot(2001U) == 1U); + CHECK(affiliations.getGrantedSlot(2002U) == 1U); + CHECK_FALSE(channels.isRFChAvailable()); +} + +TEST_CASE("DMR Tier III can recreate a grant on an exact remote payload channel", "[dmr][tier3][grant]") +{ + ::lookups::ChannelLookup channels; + REQUIRE(channels.initializeRFCh(101U)); + REQUIRE(channels.initializeRFCh(102U)); + + dmr::lookups::DMRAffiliationLookup affiliations(&channels, false); + affiliations.setSlotForChannelTSCC(101U, 1U); + + REQUIRE(affiliations.grantChSlot(2001U, 1001U, 2U, 15U, true, true, 102U)); + CHECK(affiliations.getGrantedCh(2001U) == 102U); + CHECK(affiliations.getGrantedSlot(2001U) == 2U); + CHECK_FALSE(affiliations.grantChSlot(2002U, 1002U, 2U, 15U, true, true, 102U)); +} + +TEST_CASE("DMR Tier III reserves the TSCC slot and permits its opposing payload slot", "[dmr][tier3][grant]") +{ + ::lookups::ChannelLookup channels; + REQUIRE(channels.initializeRFCh(101U)); + + dmr::lookups::DMRAffiliationLookup affiliations(&channels, false); + affiliations.setSlotForChannelTSCC(101U, 1U); + + CHECK_FALSE(channels.isRFChAvailable()); + CHECK(affiliations.getAvailableSlotForChannel(101U) == 2U); + CHECK(affiliations.getAvailableChannelForSlot(1U) == 0U); + CHECK(affiliations.getAvailableChannelForSlot(2U) == 101U); + CHECK_FALSE(affiliations.isChBusy(101U)); + + REQUIRE(affiliations.grantChSlot(2001U, 1001U, 2U, 15U, true, false)); + CHECK(affiliations.isChBusy(101U)); + CHECK(affiliations.getAvailableSlotForChannel(101U) == 0U); + + REQUIRE(affiliations.releaseGrant(2001U, false)); + CHECK_FALSE(affiliations.isChBusy(101U)); + CHECK_FALSE(channels.isRFChAvailable()); +} + +TEST_CASE("DMR Tier III release callback reports physical channel and slot", "[dmr][tier3][grant]") +{ + ::lookups::ChannelLookup channels; + REQUIRE(channels.initializeRFCh(101U)); + + dmr::lookups::DMRAffiliationLookup affiliations(&channels, false); + uint32_t releasedCh = 0U; + uint32_t releasedSrc = 0U; + uint32_t releasedDst = 0U; + uint8_t releasedSlot = 0U; + affiliations.setReleaseGrantCallback( + [&](uint32_t chNo, uint32_t srcId, uint32_t dstId, uint8_t slot) { + releasedCh = chNo; + releasedSrc = srcId; + releasedDst = dstId; + releasedSlot = slot; + }); + + REQUIRE(affiliations.grantChSlot(2001U, 1001U, 2U, 15U, false, true)); + CHECK_FALSE(affiliations.isGroup(2001U)); + CHECK(affiliations.isNetGranted(2001U)); + REQUIRE(affiliations.releaseGrant(2001U, false)); + + CHECK(releasedCh == 101U); + CHECK(releasedSrc == 1001U); + CHECK(releasedDst == 2001U); + CHECK(releasedSlot == 2U); + CHECK(affiliations.getGrantedRFChCnt() == 0U); +} + +TEST_CASE("DMR Tier III bulk release clears grants across carriers", "[dmr][tier3][grant]") +{ + ::lookups::ChannelLookup channels; + REQUIRE(channels.initializeRFCh(101U)); + REQUIRE(channels.initializeRFCh(102U)); + + dmr::lookups::DMRAffiliationLookup affiliations(&channels, false); + REQUIRE(affiliations.grantChSlot(2001U, 1001U, 1U, 15U, true, false)); + REQUIRE(affiliations.grantChSlot(2002U, 1002U, 1U, 15U, true, false)); + CHECK(affiliations.getGrantedRFChCnt() == 2U); + + REQUIRE(affiliations.releaseGrant(0U, true)); + CHECK_FALSE(affiliations.isGranted(2001U)); + CHECK_FALSE(affiliations.isGranted(2002U)); + CHECK(affiliations.getGrantedSlot(2001U) == 0U); + CHECK(affiliations.getGrantedSlot(2002U) == 0U); + CHECK(affiliations.getGrantedRFChCnt() == 0U); + CHECK(channels.isRFChAvailable()); +} diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index 1e521626..e2471961 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -163,6 +163,29 @@ bool HostTestHooks::dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_ return slot.processFrame(frame, sizeof(frame)); } +/* Configures a co-located DMR Tier III control channel. */ + +void HostTestHooks::dmrEnableTier3(dmr::Control& control, uint32_t channelNo, uint8_t tsccSlot) +{ + control.m_enableTSCC = true; + control.m_tsccSlotNo = tsccSlot; + dmr::Slot::s_channelNo = channelNo; + + dmr::Slot* slot = tsccSlot == 1U ? control.m_slot1 : control.m_slot2; + slot->setTSCC(true, false); +} + +/* Gets the active DMR Tier III payload destination. */ + +uint32_t HostTestHooks::dmrPayloadDstId(const dmr::Slot& slot) { return slot.m_tsccPayloadDstId; } + +/* Tests whether a DMR Tier III grant exists. */ + +bool HostTestHooks::dmrIsGranted(const dmr::Slot& slot, uint32_t dstId) +{ + return slot.s_affiliations->isGranted(dstId); +} + /* Gets a P25 Phase 2 slot for tests. */ p25::phase2::Slot& HostTestHooks::p25P2Slot(p25::phase2::Control& control, uint32_t slotNo) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index fa350689..5c55c7ab 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -121,6 +121,18 @@ public: * @return bool True if frame processing succeeds. */ static bool dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group = true); + /** + * @brief Configures one slot as a co-located Tier III control channel for tests. + */ + static void dmrEnableTier3(dmr::Control& control, uint32_t channelNo, uint8_t tsccSlot); + /** + * @brief Gets the active Tier III payload destination for a slot. + */ + static uint32_t dmrPayloadDstId(const dmr::Slot& slot); + /** + * @brief Tests whether the shared DMR affiliation table contains a grant. + */ + static bool dmrIsGranted(const dmr::Slot& slot, uint32_t dstId); /** @} */ /**