diff --git a/configs/config.example.yml b/configs/config.example.yml index 8a27f84f..6c6f9635 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -281,6 +281,10 @@ protocols: enableTimeDateAnn: false # Flag indicating whether or not the source ID validation before granting disabled. disableGrantSourceIdCheck: false + # Flag indicating whether successful RF group grants may steer the conventional channel talkgroup. + # This applies only when control is enabled and is not dedicated. Active RF grants remain exclusive; + # a different-TG RF grant may preempt a network-originated grant or residual RF talkgroup hang. + allowRFGrantTGSteering: false # Flag indicating whether or not the adjacent site broadcasts are disabled. disableAdjSiteBroadcast: false # Flag indicating a dedicated CC while process call TDUs and explicitly release grants when a call TDU occurs. diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index 4c1ce4ad..c666beba 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -1292,6 +1292,15 @@ bool Voice::checkRFTrafficCollision(uint32_t dstId) } } + // RF traffic on a different talkgroup preempts the active network call. + if (m_slot->m_netState != RS_NET_IDLE) { + m_slot->m_netState = RS_NET_IDLE; + m_slot->m_netTimeoutTimer.stop(); + m_slot->m_netTimeout = false; + m_slot->m_netTGHang.stop(); + m_slot->m_networkWatchdog.stop(); + } + return false; } diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index fd8f1e7b..ec0a72fc 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -1220,6 +1220,11 @@ bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId) LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId, m_nxdn->m_netLastDstId); resetNet(); + m_nxdn->m_netState = RS_NET_IDLE; + m_nxdn->m_netTimeoutTimer.stop(); + m_nxdn->m_netTimeout = false; + m_nxdn->m_netTGHang.stop(); + m_nxdn->m_networkWatchdog.stop(); if (m_nxdn->m_network != nullptr) m_nxdn->m_network->resetNXDN(); } diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 5dea949a..3264353f 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -77,6 +77,7 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q m_voiceOnControl(false), m_controlOnly(false), m_ackTSBKRequests(true), + m_allowRFGrantTGSteering(false), m_disableNetworkGrant(false), m_disableNetworkHDU(false), m_allowExplicitSourceId(false), @@ -306,6 +307,11 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw } m_ackTSBKRequests = control["ackRequests"].as(true); + m_allowRFGrantTGSteering = control["allowRFGrantTGSteering"].as(false); + if (m_allowRFGrantTGSteering && (!m_enableControl || m_dedicatedControl)) { + LogWarning(LOG_P25, "RF grant talkgroup steering requires non-dedicated control; disabling it."); + m_allowRFGrantTGSteering = false; + } m_control->m_ctrlTSDUMBF = !control["disableTSDUMBF"].as(false); if (m_isModemDFSI) m_control->m_ctrlTSDUMBF = false; // force single block TSDUs for DFSI mode @@ -515,6 +521,9 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw if (!m_control->m_ctrlTSDUMBF) { LogInfo(" Disable Multi-Block TSDUs: yes"); } + if (m_allowRFGrantTGSteering) { + LogInfo(" RF Grant Talkgroup Steering: yes"); + } LogInfo(" Time/Date Announcement TSBK: %s", m_control->m_ctrlTimeDateAnn ? "yes" : "no"); LogInfo(" Inhibit Unauthorized: %s", m_inhibitUnauth ? "yes" : "no"); diff --git a/src/host/p25/Control.h b/src/host/p25/Control.h index 78de7c0d..1986a323 100644 --- a/src/host/p25/Control.h +++ b/src/host/p25/Control.h @@ -319,6 +319,7 @@ namespace p25 bool m_voiceOnControl; bool m_controlOnly; bool m_ackTSBKRequests; + bool m_allowRFGrantTGSteering; bool m_disableNetworkGrant; bool m_disableNetworkHDU; bool m_allowExplicitSourceId; diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 97662099..f9f1a8c1 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -2153,6 +2153,34 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag uint8_t priority = ((serviceOptions & 0xFFU) & 0x07U); // Priority + const bool rfGrantSteering = m_p25->m_allowRFGrantTGSteering && m_p25->m_enableControl && !m_p25->m_dedicatedControl && !net && grp; + uint32_t activeGrantDstId = 0U; + bool activeGrantFromNetwork = false; + bool preemptNetworkGrant = false; + bool preemptNetworkTraffic = false; + + // determine the active grant destination ID and network preemption status + if (rfGrantSteering) { + if (m_p25->m_rfLastDstId != 0U && m_p25->m_affiliations->isGranted(m_p25->m_rfLastDstId)) { + activeGrantDstId = m_p25->m_rfLastDstId; + } + else if (m_p25->m_netLastDstId != 0U && m_p25->m_affiliations->isGranted(m_p25->m_netLastDstId)) { + activeGrantDstId = m_p25->m_netLastDstId; + } + else { + std::unordered_map grantTable = m_p25->m_affiliations->grantTable(); + for (const auto& grant : grantTable) { + if (grant.second == m_p25->m_siteData.channelNo() || grantTable.size() == 1U) { + activeGrantDstId = grant.first; + break; + } + } + } + + activeGrantFromNetwork = activeGrantDstId != 0U && m_p25->m_affiliations->isNetGranted(activeGrantDstId); + preemptNetworkTraffic = m_p25->m_netState != RS_NET_IDLE && m_p25->m_netLastDstId != 0U && m_p25->m_netLastDstId != dstId; + } + if (dstId == TGID_ALL) { return true; // do not generate grant packets for $FFFF (All Call) TGID } @@ -2193,6 +2221,32 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ return false; } + // check if an RF grant is currently active and if it should preempt network traffic + if (rfGrantSteering && activeGrantDstId != 0U) { + if (!activeGrantFromNetwork) { + // An RF-originated grant owns the channel until it is released or expires. + if (activeGrantDstId != dstId) { + LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denied, RF grant active, grantedDstId = %u, dstId = %u", + activeGrantDstId, dstId); + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_COLLIDE, TSBKO::IOSP_GRP_VCH, true, true); + ::ActivityLog("P25", true, "group grant request from %u to TG %u denied", srcId, dstId); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } + } + else if (activeGrantDstId == dstId) { + // matching network traffic retains ownership to avoid treating a duplicate as a new RF call + LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denied, matching network grant active, dstId = %u", dstId); + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_COLLIDE, TSBKO::IOSP_GRP_VCH, true, true); + ::ActivityLog("P25", true, "group grant request from %u to TG %u denied", srcId, dstId); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } + else { + preemptNetworkGrant = true; + } + } + // only do the last destination ID checking if we're operating in non-dedicated mode (e.g. DVRS) if (!m_p25->m_dedicatedControl) { if (m_p25->m_netState != RS_NET_IDLE && dstId == m_p25->m_netLastDstId) { @@ -2212,7 +2266,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ // don't transmit grants if the destination ID's don't match and the network TG hang timer is running if (m_p25->m_rfLastDstId != 0U) { - if (m_p25->m_rfLastDstId != dstId && (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired())) { + if (m_p25->m_rfLastDstId != dstId && (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired()) && !rfGrantSteering) { if (!net) { writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true); m_p25->m_rfState = RS_RF_REJECTED; @@ -2260,6 +2314,30 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } + // preempt network traffic or grant if necessary + if (preemptNetworkGrant || preemptNetworkTraffic) { + LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request), preempting network traffic/grant, grantedDstId = %u, dstId = %u", + activeGrantDstId, dstId); + + if (preemptNetworkGrant) + m_p25->m_affiliations->releaseGrant(activeGrantDstId, false); + + if (m_p25->m_netState != RS_NET_IDLE) { + m_p25->m_voice->resetNet(); + m_p25->m_netTimeoutTimer.stop(); + m_p25->m_netTimeout = false; + m_p25->m_netTGHang.stop(); + m_p25->m_networkWatchdog.stop(); + m_p25->m_netState = RS_NET_IDLE; + + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + + if (m_p25->m_duplex) + m_p25->writeRF_TDU(true); + } + } + if (!m_p25->m_affiliations->rfCh()->isRFChAvailable()) { if (grp) { if (!net) { @@ -2550,6 +2628,13 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } + // handle RF grant steering if applicable + if (rfGrantSteering && chNo > 0U) { + m_p25->m_rfLastSrcId = srcId; + m_p25->m_rfLastDstId = dstId; + m_p25->m_rfTGHang.start(); + } + return true; } diff --git a/tests/dmr/HostControl_Tests.cpp b/tests/dmr/HostControl_Tests.cpp index 2000b4a9..0e5ce228 100644 --- a/tests/dmr/HostControl_Tests.cpp +++ b/tests/dmr/HostControl_Tests.cpp @@ -759,13 +759,39 @@ TEST_CASE("DMR clearRFReject only clears the targeted slot", "[dmr][host][contro TEST_CASE("DMR slot rejects mismatched network traffic while RF hang is active", "[dmr][host][control][rf]") { DMRHostHarness harness; - HostTestHooks::dmrSetRFCall(*HostTestHooks::dmrSlot1(*harness.m_control), 3001U, 4001U); + HostTestHooks::dmrSetRFHang(*HostTestHooks::dmrSlot1(*harness.m_control), 3001U, 4001U); HostTestHooks::dmrStartNetVoiceCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1001U, 2001U); REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE); } +TEST_CASE("DMR slot admits matching network traffic during RF hang", "[dmr][host][control][rf-steering]") +{ + DMRHostHarness harness; + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + HostTestHooks::dmrSetRFHang(*slot, 3001U, 4001U); + + HostTestHooks::dmrStartNetVoiceCall(*slot, 1001U, 4001U); + + REQUIRE(HostTestHooks::dmrNetState(*slot) == RS_NET_AUDIO); + REQUIRE_FALSE(HostTestHooks::dmrRFTGHang(*slot).isRunning()); +} + +TEST_CASE("DMR RF traffic preempts only a different active network talkgroup", "[dmr][host][control][rf-steering]") +{ + DMRHostHarness harness; + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + + HostTestHooks::dmrStartNetVoiceCall(*slot, 1001U, 2001U); + REQUIRE_FALSE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1002U, 2001U)); + REQUIRE(HostTestHooks::dmrNetState(*slot) == RS_NET_AUDIO); + + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1002U, 2002U)); + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_AUDIO); + REQUIRE(HostTestHooks::dmrNetState(*slot) == RS_NET_IDLE); +} + TEST_CASE("DMR processFrame accepts a synthetic RF voice call on the targeted slot", "[dmr][host][control][rf]") { DMRHostHarness harness; diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index e2471961..47a3b89c 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -91,6 +91,16 @@ void HostTestHooks::dmrSetRFCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId slot.m_rfTGHang.start(); } +/* Forces DMR slot into residual RF talkgroup hang state. */ + +void HostTestHooks::dmrSetRFHang(dmr::Slot& slot, uint32_t srcId, uint32_t dstId) +{ + slot.m_rfState = RS_RF_LISTENING; + slot.m_rfLastSrcId = srcId; + slot.m_rfLastDstId = dstId; + slot.m_rfTGHang.start(); +} + /* Returns DMR slot to RF listening state. */ void HostTestHooks::dmrClearRFCall(dmr::Slot& slot) @@ -351,6 +361,40 @@ Timer& HostTestHooks::p25RFTGHang(p25::Control& control) { return control.m_rfTG Timer& HostTestHooks::p25RFLossWatchdog(p25::Control& control) { return control.m_rfLossWatchdog; } +/* Configures P25 RF grant talkgroup steering for a test. */ + +void HostTestHooks::p25ConfigureRFGrantSteering(p25::Control& control, bool enabled, bool dedicated) +{ + control.m_enableControl = true; + control.m_dedicatedControl = dedicated; + control.m_allowRFGrantTGSteering = enabled; +} + +/* Tests whether the P25 affiliation table contains a grant. */ + +bool HostTestHooks::p25IsGranted(const p25::Control& control, uint32_t dstId) +{ + return control.m_affiliations->isGranted(dstId); +} + +/* Seeds a P25 grant for a steering-policy test. */ + +bool HostTestHooks::p25SeedGrant(p25::Control& control, uint32_t srcId, uint32_t dstId, bool netGranted) +{ + return control.m_affiliations->grantCh(dstId, srcId, 15U, true, netGranted); +} + +/* Releases a P25 grant for a steering-policy test. */ + +bool HostTestHooks::p25ReleaseGrant(p25::Control& control, uint32_t dstId) +{ + return control.m_affiliations->releaseGrant(dstId, false); +} + +/* Gets the last P25 RF destination ID. */ + +uint32_t HostTestHooks::p25RFLastDstId(const p25::Control& control) { return control.m_rfLastDstId; } + /* Forces P25 control network state and last IDs. */ void HostTestHooks::p25SetNetState(p25::Control& control, RPT_NET_STATE netState, uint32_t srcId, uint32_t dstId) @@ -491,6 +535,16 @@ void HostTestHooks::nxdnSetRFCall(nxdn::Control& control, uint32_t srcId, uint32 control.m_rfTGHang.start(); } +/* Forces NXDN control into residual RF talkgroup hang state. */ + +void HostTestHooks::nxdnSetRFHang(nxdn::Control& control, uint32_t srcId, uint32_t dstId) +{ + control.m_rfState = RS_RF_LISTENING; + control.m_rfLastSrcId = srcId; + control.m_rfLastDstId = dstId; + control.m_rfTGHang.start(); +} + /* Returns NXDN control to RF listening state. */ void HostTestHooks::nxdnClearRFCall(nxdn::Control& control) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index 5c55c7ab..3d4419f5 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -94,6 +94,13 @@ public: * @param dstId Destination ID to apply to slot state. */ static void dmrSetRFCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId); + /** + * @brief Forces a DMR slot into residual RF talkgroup hang state for tests. + * @param slot DMR slot instance. + * @param srcId Source ID to retain as the last RF source. + * @param dstId Destination ID to retain as the last RF destination. + */ + static void dmrSetRFHang(dmr::Slot& slot, uint32_t srcId, uint32_t dstId); /** * @brief Returns a DMR slot to RF listening state after a synthetic RF call. * @param slot DMR slot instance. @@ -202,6 +209,42 @@ public: * @return Timer& Reference to the RF loss watchdog timer. */ static Timer& p25RFLossWatchdog(p25::Control& control); + /** + * @brief Configures P25 RF grant talkgroup steering for a test. + * @param control P25 control instance. + * @param enabled Flag indicating whether RF grant steering is enabled. + * @param dedicated Flag indicating whether the control channel is dedicated. + */ + static void p25ConfigureRFGrantSteering(p25::Control& control, bool enabled, bool dedicated); + /** + * @brief Tests whether the P25 affiliation table contains a grant. + * @param control P25 control instance. + * @param dstId Destination ID to test. + * @returns bool True if the destination is granted; otherwise, false. + */ + static bool p25IsGranted(const p25::Control& control, uint32_t dstId); + /** + * @brief Seeds a P25 grant for a steering-policy test. + * @param control P25 control instance. + * @param srcId Source radio ID. + * @param dstId Destination ID. + * @param netGranted Flag indicating whether the grant originated from the network. + * @returns bool True if the grant was created; otherwise, false. + */ + static bool p25SeedGrant(p25::Control& control, uint32_t srcId, uint32_t dstId, bool netGranted); + /** + * @brief Releases a P25 grant for a steering-policy test. + * @param control P25 control instance. + * @param dstId Destination ID to release. + * @returns bool True if the grant was released; otherwise, false. + */ + static bool p25ReleaseGrant(p25::Control& control, uint32_t dstId); + /** + * @brief Gets the last P25 RF destination ID. + * @param control P25 control instance. + * @returns uint32_t Last RF destination ID. + */ + static uint32_t p25RFLastDstId(const p25::Control& control); /** * @brief Forces P25 control network state and last IDs for targeted recovery-path tests. * @param control P25 control instance. @@ -466,6 +509,13 @@ public: * @param dstId Destination ID to apply to control state. */ static void nxdnSetRFCall(nxdn::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Forces NXDN control into residual RF talkgroup hang state for tests. + * @param control NXDN control instance. + * @param srcId Source ID to retain as the last RF source. + * @param dstId Destination ID to retain as the last RF destination. + */ + static void nxdnSetRFHang(nxdn::Control& control, uint32_t srcId, uint32_t dstId); /** * @brief Returns NXDN control to RF listening state after a synthetic RF call. * @param control NXDN control instance. diff --git a/tests/nxdn/HostControl_Tests.cpp b/tests/nxdn/HostControl_Tests.cpp index 38a824b3..7af357dd 100644 --- a/tests/nxdn/HostControl_Tests.cpp +++ b/tests/nxdn/HostControl_Tests.cpp @@ -882,7 +882,7 @@ TEST_CASE("NXDN clearRFReject returns rejected RF state to listening", "[nxdn][h TEST_CASE("NXDN rejects mismatched network traffic while RF hang is active", "[nxdn][host][control][rf]") { NXDNHostHarness harness; - HostTestHooks::nxdnSetRFCall(*harness.m_control, 3001U, 4001U); + HostTestHooks::nxdnSetRFHang(*harness.m_control, 3001U, 4001U); nxdn::lc::RTCH control; control.setMessageType(nxdn::defines::MessageType::RTCH_VCALL); @@ -895,6 +895,36 @@ TEST_CASE("NXDN rejects mismatched network traffic while RF hang is active", "[n REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE); } +TEST_CASE("NXDN admits matching network traffic during RF hang", "[nxdn][host][control][rf-steering]") +{ + NXDNHostHarness harness; + HostTestHooks::nxdnSetRFHang(*harness.m_control, 3001U, 4001U); + + nxdn::lc::RTCH control; + control.setMessageType(nxdn::defines::MessageType::RTCH_VCALL); + control.setSrcId(1001U); + control.setDstId(4001U); + control.setGroup(true); + control.setTransmissionMode(nxdn::defines::TransmissionMode::MODE_4800); + + REQUIRE(HostTestHooks::nxdnStartNetCall(*harness.m_control, control)); + REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO); + REQUIRE_FALSE(HostTestHooks::nxdnRFTGHang(*harness.m_control).isRunning()); +} + +TEST_CASE("NXDN RF traffic preempts only a different active network talkgroup", "[nxdn][host][control][rf-steering]") +{ + NXDNHostHarness harness; + + harness.startNetworkVoiceCall(1001U, 2001U); + REQUIRE_FALSE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1002U, 2001U)); + REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO); + + REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1002U, 2002U)); + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO); + REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE); +} + TEST_CASE("NXDN processFrame accepts a synthetic RF voice call", "[nxdn][host][control][rf]") { NXDNHostHarness harness; diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 53664389..d08a8ad1 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -476,6 +476,121 @@ TEST_CASE("P25 active talkgroup updates preserve the working TGID", "[p25][activ REQUIRE(dstIdB == 0U); } +TEST_CASE("P25 enhanced conventional RF grants steer without replacing RF ownership", "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, false); + + harness.m_control->grantTG(1001U, 2001U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2001U); + REQUIRE(HostTestHooks::p25RFTGHang(*harness.m_control).isRunning()); + + harness.m_control->grantTG(1002U, 2002U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + REQUIRE_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, 2002U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2001U); +} + +TEST_CASE("P25 enhanced conventional RF grant preempts a different network grant", "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, false); + REQUIRE(HostTestHooks::p25SeedGrant(*harness.m_control, 1001U, 2001U, true)); + HostTestHooks::p25SetNetState(*harness.m_control, RS_NET_AUDIO, 1001U, 2001U); + + harness.m_control->grantTG(1002U, 2002U, true); + REQUIRE_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2002U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2002U); + REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE); +} + +TEST_CASE("P25 RF grant steering overrides hang only on non-dedicated control", "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, false); + + harness.m_control->grantTG(1001U, 2001U, true); + REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, 2001U)); + harness.m_control->grantTG(1002U, 2002U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2002U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2002U); + + REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, 2002U)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, true); + harness.m_control->grantTG(1003U, 2003U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2003U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2002U); +} + +TEST_CASE("P25 disabled RF grant steering preserves conventional hang ownership", "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, false); + + harness.m_control->grantTG(1001U, 2001U, true); + REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, 2001U)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, false, false); + + harness.m_control->grantTG(1002U, 2002U, true); + REQUIRE_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, 2002U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2001U); + REQUIRE(HostTestHooks::p25RFTGHang(*harness.m_control).isRunning()); +} + +TEST_CASE("P25 enhanced conventional RF grant cannot replace matching network ownership", "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, false); + REQUIRE(HostTestHooks::p25SeedGrant(*harness.m_control, 1001U, 2001U, true)); + + harness.m_control->grantTG(1002U, 2001U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 0U); +} + +TEST_CASE("P25 enhanced conventional RF grant permits only the owning source to retry", "[p25][host][control][grant-steering]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, false); + + harness.m_control->grantTG(1001U, 2001U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + + harness.m_control->grantTG(1001U, 2001U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2001U); + + harness.m_control->grantTG(1002U, 2001U, true); + REQUIRE(HostTestHooks::p25IsGranted(*harness.m_control, 2001U)); + REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2001U); +} + TEST_CASE("P25 host preserves encrypted network call parameters", "[p25][host][control][crypto]") { P25HostHarness harness;