diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 879ccef9..a77b5928 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -1024,7 +1024,7 @@ void Control::clock() if (m_networkWatchdog.isRunning()) { m_networkWatchdog.clock(ms); - if (m_networkWatchdog.hasExpired()) { + if (m_networkWatchdog.hasExpired() && !(m_netState == RS_NET_IDLE && m_rfState != RS_RF_LISTENING)) { if (m_netState == RS_NET_AUDIO) { if (m_voice->m_netFrames > 0.0F) { ::ActivityLog("P25", false, "network watchdog has expired, %.1f seconds, %u%% packet loss", @@ -1454,6 +1454,7 @@ void Control::processNetwork() if (m_netState != RS_NET_DATA) { // don't process network frames if the RF modem isn't in a listening state if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) { + m_networkWatchdog.start(); return; } } diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index ab9bec8c..176efdbc 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -196,6 +196,16 @@ void HostTestHooks::p25SetRFCall(p25::Control& control, uint32_t srcId, uint32_t control.m_rfTGHang.start(); } +/* Returns P25 control to RF listening state. */ + +void HostTestHooks::p25ClearRFCall(p25::Control& control) +{ + control.m_rfState = RS_RF_LISTENING; + control.m_rfLastSrcId = 0U; + control.m_rfLastDstId = 0U; + control.m_rfTGHang.stop(); +} + /* Forces P25 control into RF rejected state. */ void HostTestHooks::p25SetRFRejected(p25::Control& control) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index 95a85788..bd703f8f 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -171,6 +171,11 @@ public: * @param dstId Destination ID to apply to control state. */ static void p25SetRFCall(p25::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Returns P25 control to the RF listening state after a synthetic RF call. + * @param control P25 control instance. + */ + static void p25ClearRFCall(p25::Control& control); /** * @brief Forces P25 control into RF rejected state for tests. * @param control P25 control instance. diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 425e6709..da881970 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -656,6 +656,123 @@ TEST_CASE("P25 host e2e loopback times out a stream before call state starts", " REQUIRE(harness.network()->rxP25StreamId() == nextStream); } +TEST_CASE("P25 host e2e loopback preserves a continuing network stream across an RF collision", "[p25][host][control][net][e2e]") +{ + const uint16_t hostPort = reserveLoopbackPort(); + const uint16_t senderPort = reserveLoopbackPort(); + REQUIRE(hostPort != 0U); + REQUIRE(senderPort != 0U); + REQUIRE(hostPort != senderPort); + + const uint32_t hostPeerId = 6009U; + const uint32_t streamId = 0x500201U; + + P25HostHarness harness(true, true, hostPort, hostPeerId); + P25TestNetwork sender(senderPort, 6010U); + + REQUIRE(harness.network() != nullptr); + REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort)); + REQUIRE(sender.activateLoopback("127.0.0.1", hostPort)); + + HostTestHooks::p25SetRFCall(*harness.m_control, 1401U, 2401U); + REQUIRE(sender.sendP25LDU1Frame(hostPeerId, streamId, 600U, 1501U, 2501U)); + + for (uint32_t i = 0U; i < 40U; i++) { + sender.clock(1U); + harness.network()->clock(1U); + harness.m_control->clock(); + if (harness.network()->rxP25StreamId() == streamId && + HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning()) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + + REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE); + REQUIRE(harness.network()->rxP25StreamId() == streamId); + REQUIRE(harness.network()->resetP25Count() == 0U); + + HostTestHooks::p25NetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetworkWatchdog(*harness.m_control))); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).hasExpired()); + REQUIRE(harness.network()->rxP25StreamId() == streamId); + REQUIRE(harness.network()->resetP25Count() == 0U); + + HostTestHooks::p25ClearRFCall(*harness.m_control); + REQUIRE(sender.sendP25LDU1Frame(hostPeerId, streamId, 601U, 1501U, 2501U)); + REQUIRE(sender.sendP25LDU2Frame(hostPeerId, streamId, 602U, 1501U, 2501U)); + + for (uint32_t i = 0U; i < 40U; i++) { + sender.clock(1U); + harness.network()->clock(1U); + harness.m_control->clock(); + if (HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + + REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO); + REQUIRE(harness.network()->rxP25StreamId() == streamId); + REQUIRE(harness.network()->resetP25Count() == 0U); +} + +TEST_CASE("P25 host e2e loopback clears a stopped network stream after an RF collision", "[p25][host][control][net][e2e]") +{ + const uint16_t hostPort = reserveLoopbackPort(); + const uint16_t senderPort = reserveLoopbackPort(); + REQUIRE(hostPort != 0U); + REQUIRE(senderPort != 0U); + REQUIRE(hostPort != senderPort); + + const uint32_t hostPeerId = 6011U; + const uint32_t streamId = 0x500202U; + + P25HostHarness harness(true, true, hostPort, hostPeerId); + P25TestNetwork sender(senderPort, 6012U); + + REQUIRE(harness.network() != nullptr); + REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort)); + REQUIRE(sender.activateLoopback("127.0.0.1", hostPort)); + + HostTestHooks::p25SetRFCall(*harness.m_control, 1401U, 2401U); + REQUIRE(sender.sendP25LDU1Frame(hostPeerId, streamId, 700U, 1501U, 2501U)); + + for (uint32_t i = 0U; i < 40U; i++) { + sender.clock(1U); + harness.network()->clock(1U); + harness.m_control->clock(); + if (harness.network()->rxP25StreamId() == streamId && + HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning()) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + + REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE); + REQUIRE(harness.network()->rxP25StreamId() == streamId); + REQUIRE(harness.network()->resetP25Count() == 0U); + + HostTestHooks::p25NetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetworkWatchdog(*harness.m_control))); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).hasExpired()); + REQUIRE(harness.network()->rxP25StreamId() == streamId); + REQUIRE(harness.network()->resetP25Count() == 0U); + + HostTestHooks::p25ClearRFCall(*harness.m_control); + harness.m_control->clock(); + + REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE); + REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning()); + REQUIRE(harness.network()->rxP25StreamId() == 0U); + REQUIRE(harness.network()->resetP25Count() == 1U); +} + TEST_CASE("P25 host e2e loopback enforces stream lock until active stream terminates", "[p25][host][control][net][e2e]") { const uint16_t hostPort = reserveLoopbackPort();