From 2cff88b802a7aac1928249b132f1b9c83a6489da Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Wed, 5 Aug 2026 15:49:12 -0400 Subject: [PATCH] add rfTGHang and rfLossWatchdog tests; --- tests/dmr/HostControl_Tests.cpp | 38 +++++++++++++++++++++++++ tests/host/HostTestHooks.cpp | 24 ++++++++++++++++ tests/host/HostTestHooks.h | 36 +++++++++++++++++++++++ tests/nxdn/HostControl_Tests.cpp | 36 +++++++++++++++++++++++ tests/p25/HostControl_Tests.cpp | 49 ++++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+) diff --git a/tests/dmr/HostControl_Tests.cpp b/tests/dmr/HostControl_Tests.cpp index 6da8b5ff..f1fe9e29 100644 --- a/tests/dmr/HostControl_Tests.cpp +++ b/tests/dmr/HostControl_Tests.cpp @@ -773,4 +773,42 @@ TEST_CASE("DMR processFrame accepts a synthetic RF voice call on the targeted sl REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1001U, 2001U)); REQUIRE(HostTestHooks::dmrRFState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_RF_AUDIO); REQUIRE(harness.m_control->getLastDstId(1U) == 2001U); +} + +TEST_CASE("DMR rfTGHang expiry ends active RF call and returns slot to listening", "[dmr][host][control][rf]") +{ + DMRHostHarness harness; + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1001U, 2001U)); + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_AUDIO); + + HostTestHooks::dmrRFTGHang(*slot).start(); + HostTestHooks::dmrRFTGHang(*slot).clock(expireTimerTicks(HostTestHooks::dmrRFTGHang(*slot))); + slot->clock(); + + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_LISTENING); + REQUIRE_FALSE(HostTestHooks::dmrRFTGHang(*slot).isRunning()); + REQUIRE_FALSE(HostTestHooks::dmrRFLossWatchdog(*slot).isRunning()); +} + +TEST_CASE("DMR rfTGHang zero fallback arms rfLossWatchdog and recovers RF state", "[dmr][host][control][rf]") +{ + DMRHostHarness harness; + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1101U, 2101U)); + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_AUDIO); + + HostTestHooks::dmrRFTGHang(*slot).setTimeout(0U); + REQUIRE_FALSE(HostTestHooks::dmrRFLossWatchdog(*slot).isRunning()); + + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1101U, 2101U)); + REQUIRE(HostTestHooks::dmrRFLossWatchdog(*slot).isRunning()); + + HostTestHooks::dmrRFLossWatchdog(*slot).clock(expireTimerTicks(HostTestHooks::dmrRFLossWatchdog(*slot))); + slot->clock(); + + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_LISTENING); + REQUIRE_FALSE(HostTestHooks::dmrRFLossWatchdog(*slot).isRunning()); } \ No newline at end of file diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index ed3865ae..e642467e 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -73,6 +73,14 @@ Timer& HostTestHooks::dmrNetworkWatchdog(dmr::Slot& slot) { return slot.m_networ Timer& HostTestHooks::dmrNetTGHang(dmr::Slot& slot) { return slot.m_netTGHang; } +/* Gets DMR RF talkgroup hang timer. */ + +Timer& HostTestHooks::dmrRFTGHang(dmr::Slot& slot) { return slot.m_rfTGHang; } + +/* Gets DMR RF loss watchdog timer. */ + +Timer& HostTestHooks::dmrRFLossWatchdog(dmr::Slot& slot) { return slot.m_rfLossWatchdog; } + /* Forces DMR slot into active RF call state. */ void HostTestHooks::dmrSetRFCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId) @@ -187,6 +195,14 @@ Timer& HostTestHooks::p25NetworkWatchdog(p25::Control& control) { return control Timer& HostTestHooks::p25NetTGHang(p25::Control& control) { return control.m_netTGHang; } +/* Gets P25 RF talkgroup hang timer. */ + +Timer& HostTestHooks::p25RFTGHang(p25::Control& control) { return control.m_rfTGHang; } + +/* Gets P25 RF loss watchdog timer. */ + +Timer& HostTestHooks::p25RFLossWatchdog(p25::Control& control) { return control.m_rfLossWatchdog; } + /* Forces P25 control network state and last IDs. */ void HostTestHooks::p25SetNetState(p25::Control& control, RPT_NET_STATE netState, uint32_t srcId, uint32_t dstId) @@ -281,6 +297,14 @@ Timer& HostTestHooks::nxdnNetworkWatchdog(nxdn::Control& control) { return contr Timer& HostTestHooks::nxdnNetTGHang(nxdn::Control& control) { return control.m_netTGHang; } +/* Gets NXDN RF talkgroup hang timer. */ + +Timer& HostTestHooks::nxdnRFTGHang(nxdn::Control& control) { return control.m_rfTGHang; } + +/* Gets NXDN RF loss watchdog timer. */ + +Timer& HostTestHooks::nxdnRFLossWatchdog(nxdn::Control& control) { return control.m_rfLossWatchdog; } + /* Forces NXDN control into active RF call state. */ void HostTestHooks::nxdnSetRFCall(nxdn::Control& control, uint32_t srcId, uint32_t dstId) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index a1d21e04..f5128cc8 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -73,6 +73,18 @@ public: * @return Timer& Reference to the network TG hang timer. */ static Timer& dmrNetTGHang(dmr::Slot& slot); + /** + * @brief Gets the DMR RF talkgroup hang timer for a slot. + * @param slot DMR slot instance. + * @return Timer& Reference to the RF TG hang timer. + */ + static Timer& dmrRFTGHang(dmr::Slot& slot); + /** + * @brief Gets the DMR RF loss watchdog timer for a slot. + * @param slot DMR slot instance. + * @return Timer& Reference to the RF loss watchdog timer. + */ + static Timer& dmrRFLossWatchdog(dmr::Slot& slot); /** * @brief Forces a DMR slot into active RF call state for tests. * @param slot DMR slot instance. @@ -161,6 +173,18 @@ public: * @return Timer& Reference to the network TG hang timer. */ static Timer& p25NetTGHang(p25::Control& control); + /** + * @brief Gets the P25 RF talkgroup hang timer. + * @param control P25 control instance. + * @return Timer& Reference to the RF TG hang timer. + */ + static Timer& p25RFTGHang(p25::Control& control); + /** + * @brief Gets the P25 RF loss watchdog timer. + * @param control P25 control instance. + * @return Timer& Reference to the RF loss watchdog timer. + */ + static Timer& p25RFLossWatchdog(p25::Control& control); /** * @brief Forces P25 control network state and last IDs for targeted recovery-path tests. * @param control P25 control instance. @@ -256,6 +280,18 @@ public: * @return Timer& Reference to the network TG hang timer. */ static Timer& nxdnNetTGHang(nxdn::Control& control); + /** + * @brief Gets the NXDN RF talkgroup hang timer. + * @param control NXDN control instance. + * @return Timer& Reference to the RF TG hang timer. + */ + static Timer& nxdnRFTGHang(nxdn::Control& control); + /** + * @brief Gets the NXDN RF loss watchdog timer. + * @param control NXDN control instance. + * @return Timer& Reference to the RF loss watchdog timer. + */ + static Timer& nxdnRFLossWatchdog(nxdn::Control& control); /** * @brief Forces NXDN control into active RF call state for tests. * @param control NXDN control instance. diff --git a/tests/nxdn/HostControl_Tests.cpp b/tests/nxdn/HostControl_Tests.cpp index 600bc747..6e6cbf51 100644 --- a/tests/nxdn/HostControl_Tests.cpp +++ b/tests/nxdn/HostControl_Tests.cpp @@ -582,4 +582,40 @@ TEST_CASE("NXDN processFrame accepts a synthetic RF voice call", "[nxdn][host][c REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1001U, 2001U)); REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO); REQUIRE(harness.m_control->getLastDstId() == 2001U); +} + +TEST_CASE("NXDN rfTGHang expiry ends active RF call and returns to listening", "[nxdn][host][control][rf]") +{ + NXDNHostHarness harness; + + REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1001U, 2001U)); + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO); + + HostTestHooks::nxdnRFTGHang(*harness.m_control).start(); + HostTestHooks::nxdnRFTGHang(*harness.m_control).clock(expireTimerTicks(HostTestHooks::nxdnRFTGHang(*harness.m_control))); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_LISTENING); + REQUIRE_FALSE(HostTestHooks::nxdnRFTGHang(*harness.m_control).isRunning()); + REQUIRE_FALSE(HostTestHooks::nxdnRFLossWatchdog(*harness.m_control).isRunning()); +} + +TEST_CASE("NXDN rfTGHang zero fallback arms rfLossWatchdog and recovers RF state", "[nxdn][host][control][rf]") +{ + NXDNHostHarness harness; + + REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1101U, 2101U)); + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO); + + HostTestHooks::nxdnRFTGHang(*harness.m_control).setTimeout(0U); + REQUIRE_FALSE(HostTestHooks::nxdnRFLossWatchdog(*harness.m_control).isRunning()); + + REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1101U, 2101U)); + REQUIRE(HostTestHooks::nxdnRFLossWatchdog(*harness.m_control).isRunning()); + + HostTestHooks::nxdnRFLossWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::nxdnRFLossWatchdog(*harness.m_control))); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_LISTENING); + REQUIRE_FALSE(HostTestHooks::nxdnRFLossWatchdog(*harness.m_control).isRunning()); } \ No newline at end of file diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index da881970..ffe63763 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -935,3 +935,52 @@ TEST_CASE("P25 processFrame accepts calibration-based RF LDU2 frame after LDU1", REQUIRE(harness.m_control->getLastSrcId() == 1U); REQUIRE(harness.m_control->getLastDstId() == 1U); } + +TEST_CASE("P25 rfTGHang expiry ends active RF call and returns to listening", "[p25][host][control][rf]") +{ + P25HostHarness harness; + harness.m_control->reset(); + + uint8_t ldu1[p25::defines::P25_LDU_FRAME_LENGTH_BYTES + 2U]; + buildP25RFFrame(CAL_P25_LDU1_1K, ldu1); + HostTestHooks::p25StampRFFrameNID(*harness.m_control, ldu1, p25::defines::DUID::LDU1); + + REQUIRE(harness.m_control->processFrame(ldu1, sizeof(ldu1))); + REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_AUDIO); + + HostTestHooks::p25RFTGHang(*harness.m_control).start(); + HostTestHooks::p25RFTGHang(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25RFTGHang(*harness.m_control))); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_LISTENING); + REQUIRE_FALSE(HostTestHooks::p25RFTGHang(*harness.m_control).isRunning()); + REQUIRE_FALSE(HostTestHooks::p25RFLossWatchdog(*harness.m_control).isRunning()); +} + +TEST_CASE("P25 rfTGHang zero fallback arms rfLossWatchdog and recovers RF state", "[p25][host][control][rf]") +{ + P25HostHarness harness; + harness.m_control->reset(); + + uint8_t ldu1[p25::defines::P25_LDU_FRAME_LENGTH_BYTES + 2U]; + uint8_t ldu2[p25::defines::P25_LDU_FRAME_LENGTH_BYTES + 2U]; + buildP25RFFrame(CAL_P25_LDU1_1K, ldu1); + buildP25RFFrame(CAL_P25_LDU2_1K, ldu2); + HostTestHooks::p25StampRFFrameNID(*harness.m_control, ldu1, p25::defines::DUID::LDU1); + HostTestHooks::p25StampRFFrameNID(*harness.m_control, ldu2, p25::defines::DUID::LDU2); + + REQUIRE(harness.m_control->processFrame(ldu1, sizeof(ldu1))); + REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_AUDIO); + + HostTestHooks::p25RFTGHang(*harness.m_control).setTimeout(0U); + REQUIRE_FALSE(HostTestHooks::p25RFLossWatchdog(*harness.m_control).isRunning()); + + REQUIRE(harness.m_control->processFrame(ldu2, sizeof(ldu2))); + REQUIRE(HostTestHooks::p25RFLossWatchdog(*harness.m_control).isRunning()); + + HostTestHooks::p25RFLossWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25RFLossWatchdog(*harness.m_control))); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_LISTENING); + REQUIRE_FALSE(HostTestHooks::p25RFLossWatchdog(*harness.m_control).isRunning()); +}