You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dvmhost/tests/dmr/HostControl_Tests.cpp

814 lines
29 KiB

// 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/lookups/ChannelLookup.h"
#include "common/lookups/IdenTableLookup.h"
#include "common/lookups/RSSIInterpolator.h"
#include "common/lookups/RadioIdLookup.h"
#include "common/lookups/TalkgroupRulesLookup.h"
#include "common/network/Network.h"
#include "common/network/NetRPC.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/Sync.h"
#include "common/dmr/SlotType.h"
#include "common/dmr/lc/LC.h"
#include "common/dmr/lc/FullLC.h"
#include "common/dmr/data/NetData.h"
#include "host/modem/Modem.h"
#include "modem/port/IModemPort.h"
#include "host/HostTestHooks.h"
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <cstring>
#include <thread>
extern network::NetRPC* g_RPC;
#include "host/dmr/Control.h"
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
namespace {
/**
* @brief Helper function to calculate the number of timer ticks needed to expire a given timer.
* @param timer The timer for which to calculate the expiration ticks.
* @returns uint32_t The number of timer ticks needed to expire the timer.
*/
uint32_t expireTimerTicks(const Timer& timer)
{
return (timer.getTimeout() + 1U) * 1000U;
}
/**
* @brief Finds an available loopback UDP port.
* @returns uint16_t A free UDP port, or 0 on failure.
*/
uint16_t reserveLoopbackPort()
{
#if defined(_WIN32)
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd == INVALID_SOCKET)
return 0U;
#else
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return 0U;
#endif // defined(_WIN32)
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_port = htons(0U);
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
socklen_t addrLen = sizeof(address);
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return ntohs(address.sin_port);
}
/**
* @brief Builds a DMR voice header payload.
* @param payload The buffer to store the DMR voice header payload.
* @param srcId The source ID for the DMR header.
* @param dstId The destination ID for the DMR header.
* @param group True if the header is for a group call, false for a private call.
*/
void buildDMRVoiceHeaderPayload(uint8_t* payload, uint32_t srcId, uint32_t dstId, bool group)
{
using namespace dmr;
using namespace dmr::defines;
::memset(payload, 0x00U, DMR_FRAME_LENGTH_BYTES);
Sync::addDMRDataSync(payload, false);
lc::LC lc(group ? FLCO::GROUP : FLCO::PRIVATE, srcId, dstId);
lc::FullLC fullLC;
fullLC.encode(lc, payload, DataType::VOICE_LC_HEADER);
SlotType slotType;
slotType.setColorCode(1U);
slotType.setDataType(DataType::VOICE_LC_HEADER);
slotType.encode(payload);
}
/**
* @brief Builds a DMR terminator payload.
* @param payload The buffer to store the DMR terminator payload.
* @param srcId The source ID for the DMR header.
* @param dstId The destination ID for the DMR header.
* @param group True if the header is for a group call, false for a private call.
*/
void buildDMRTerminatorPayload(uint8_t* payload, uint32_t srcId, uint32_t dstId, bool group)
{
using namespace dmr;
using namespace dmr::defines;
::memset(payload, 0x00U, DMR_FRAME_LENGTH_BYTES);
Sync::addDMRDataSync(payload, false);
lc::LC lc(group ? FLCO::GROUP : FLCO::PRIVATE, srcId, dstId);
lc::FullLC fullLC;
fullLC.encode(lc, payload, DataType::TERMINATOR_WITH_LC);
SlotType slotType;
slotType.setColorCode(1U);
slotType.setDataType(DataType::TERMINATOR_WITH_LC);
slotType.encode(payload);
}
/**
* @brief Builds a DMR voice sync payload.
* @param payload The buffer to store the DMR voice sync payload.
*/
void buildDMRVoiceSyncPayload(uint8_t* payload)
{
::memset(payload, 0x00U, dmr::defines::DMR_FRAME_LENGTH_BYTES);
dmr::Sync::addDMRAudioSync(payload, false);
}
} // namespace
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Dummy implementation of a modem port for testing purposes.
*/
class DMRTestModemPort final : public modem::port::IModemPort {
public:
/**
* @brief Finalizes the instance of the DMRTestModemPort class.
*/
~DMRTestModemPort() override = default;
/**
* @brief Opens the modem port.
* @returns bool True if the modem port was successfully opened, false otherwise.
*/
bool open() override { return true; }
/**
* @brief Reads data from the modem port.
* @param buffer The buffer to store the read data.
* @param length The number of bytes to read.
* @returns int The number of bytes actually read.
*/
int read(uint8_t* buffer, uint32_t length) override
{
(void)buffer;
(void)length;
return 0;
}
/**
* @brief Writes data to the modem port.
* @param buffer The buffer containing the data to write.
* @param length The number of bytes to write.
* @returns int The number of bytes actually written.
*/
int write(const uint8_t* buffer, uint32_t length) override
{
(void)buffer;
return static_cast<int>(length);
}
/**
* @brief Closes the modem port.
*/
void close() override {}
};
/**
* @brief Lightweight network test double that records DMR reset calls.
*/
class DMRTestNetwork final : public network::Network {
public:
/**
* @brief Initializes a new instance of the DMRTestNetwork class.
* @param localPort The local port number.
* @param peerId The peer ID.
*/
DMRTestNetwork(uint16_t localPort = 0U, uint32_t peerId = 1U) :
network::Network("127.0.0.1", 1U, localPort, peerId, "test", true, true, true, false, false, false, true, true, false, false, false, false),
m_resetDMRCount(0U)
{
// keep protocol gates deterministic for this P25-focused harness
m_dmrEnabled = true;
m_p25Enabled = false;
m_nxdnEnabled = false;
m_analogEnabled = false;
}
/**
* @brief Activates the loopback network connection.
* @param remoteAddress The remote address to connect to.
* @param remotePort The remote port to connect to.
* @returns bool True if the loopback network connection was successfully activated, false otherwise.
*/
bool activateLoopback(const std::string& remoteAddress, uint16_t remotePort)
{
if (network::udp::Socket::lookup(remoteAddress, remotePort, m_addr, m_addrLen) != 0) {
return false;
}
if (!m_socket->open(m_addr.ss_family)) {
return false;
}
m_enabled = true;
m_status = network::NET_STAT_RUNNING;
return true;
}
/**
* @brief Sends a DMR network voice LC header frame.
*/
bool sendDMRVoiceHeader(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t slotNo, uint32_t srcId, uint32_t dstId, bool group = true)
{
using namespace dmr::defines;
dmr::data::NetData data;
data.setSlotNo(slotNo);
data.setSrcId(srcId);
data.setDstId(dstId);
data.setFLCO(group ? FLCO::GROUP : FLCO::PRIVATE);
data.setControl(0x00U);
data.setN(0U);
data.setSeqNo((uint8_t)(seq & 0xFFU));
data.setDataType(DataType::VOICE_LC_HEADER);
uint8_t payload[DMR_FRAME_LENGTH_BYTES];
buildDMRVoiceHeaderPayload(payload, srcId, dstId, group);
data.setData(payload);
uint32_t messageLength = 0U;
UInt8Array message = createDMR_Message(messageLength, streamId, data);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, seq, m_addr, m_addrLen);
}
/**
* @brief Sends a DMR network voice sync frame.
*/
bool sendDMRVoiceSync(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t slotNo, uint32_t srcId, uint32_t dstId, bool group = true)
{
using namespace dmr::defines;
dmr::data::NetData data;
data.setSlotNo(slotNo);
data.setSrcId(srcId);
data.setDstId(dstId);
data.setFLCO(group ? FLCO::GROUP : FLCO::PRIVATE);
data.setControl(0x00U);
data.setN(0U);
data.setSeqNo((uint8_t)(seq & 0xFFU));
data.setDataType(DataType::VOICE_SYNC);
uint8_t payload[DMR_FRAME_LENGTH_BYTES];
buildDMRVoiceSyncPayload(payload);
data.setData(payload);
uint32_t messageLength = 0U;
UInt8Array message = createDMR_Message(messageLength, streamId, data);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, seq, m_addr, m_addrLen);
}
/**
* @brief Sends a DMR network terminator frame.
*/
bool sendDMRTerminator(uint32_t targetPeerId, uint32_t streamId, uint16_t seq, uint32_t slotNo, uint32_t srcId, uint32_t dstId, bool group = true)
{
using namespace dmr::defines;
dmr::data::NetData data;
data.setSlotNo(slotNo);
data.setSrcId(srcId);
data.setDstId(dstId);
data.setFLCO(group ? FLCO::GROUP : FLCO::PRIVATE);
data.setControl(0x00U);
data.setN(0U);
data.setSeqNo((uint8_t)(seq & 0xFFU));
data.setDataType(DataType::TERMINATOR_WITH_LC);
uint8_t payload[DMR_FRAME_LENGTH_BYTES];
buildDMRTerminatorPayload(payload, srcId, dstId, group);
data.setData(payload);
uint32_t messageLength = 0U;
UInt8Array message = createDMR_Message(messageLength, streamId, data);
if (message == nullptr || messageLength == 0U) {
return false;
}
return m_frameQueue->write(message.get(), messageLength, streamId, targetPeerId, m_peerId,
{ network::NET_FUNC::PROTOCOL, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, seq, m_addr, m_addrLen);
}
/**
* @brief
*/
void resetDMR(uint32_t slotNo) override
{
++m_resetDMRCount;
network::Network::resetDMR(slotNo);
}
/**
* @brief Returns the number of times the DMR subsystem has been reset.
* @returns uint32_t The number of times the DMR subsystem has been reset.
*/
uint32_t resetDMRCount() const
{
return m_resetDMRCount;
}
/**
* @brief Returns the currently locked incoming DMR stream ID for a slot.
* @param slotNo Logical DMR slot number (1 or 2).
* @returns uint32_t Active incoming stream lock ID, or 0 when unlocked.
*/
uint32_t rxDMRStreamId(uint32_t slotNo) const
{
return m_rxDMRStreamId[slotNo - 1U];
}
private:
uint32_t m_resetDMRCount;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Harness class for testing DMR host control functionality.
*/
class DMRHostHarness {
public:
/**
* @brief Initializes a new instance of the DMRHostHarness class.
* @param authoritative Indicates whether the host is authoritative.
*/
explicit DMRHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U) :
m_rpc("127.0.0.1", 1U, 0U, "test", false),
m_modem(new DMRTestModemPort(), false, false, false, false, false, false,
0U, 0U, 0U, 4096U, 4096U, 1024U, true, true, false, false, false, false),
m_chLookup(),
m_ridLookup("", 0U, false, false),
m_tidLookup("", 0U, false, false),
m_idenLookup("", 0U),
m_rssiMapper(),
m_network(withNetwork ? new DMRTestNetwork(networkLocalPort, networkPeerId) : nullptr),
m_control(nullptr)
{
g_RPC = &m_rpc;
m_modem.setModeParams(true, false, false);
m_control = new dmr::Control(authoritative, 1U, 1U, 4096U, false, false, 5U, 2U,
&m_modem, m_network, false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup,
&m_rssiMapper, 60U, false, false, false, true, true);
}
/**
* @brief Finalizes an instance of the DMRHostHarness class.
*/
~DMRHostHarness()
{
delete m_control;
delete m_network;
g_RPC = nullptr;
}
DMRTestNetwork* network() const
{
return m_network;
}
public:
network::NetRPC m_rpc;
modem::Modem m_modem;
lookups::ChannelLookup m_chLookup;
lookups::RadioIdLookup m_ridLookup;
lookups::TalkgroupRulesLookup m_tidLookup;
lookups::IdenTableLookup m_idenLookup;
lookups::RSSIInterpolator m_rssiMapper;
DMRTestNetwork* m_network;
dmr::Control* m_control;
};
TEST_CASE("DMR host e2e loopback handles missed frames without dropping active call", "[dmr][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 = 7001U;
const uint32_t streamId = 0x610001U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
DMRTestNetwork sender(senderPort, 7002U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 100U, 1U, 1001U, 2001U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.m_control->getLastSrcId(1U) == 1001U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2001U);
// Skip one RTP sequence (101) to emulate a missing network frame.
REQUIRE(sender.sendDMRVoiceSync(hostPeerId, streamId, 102U, 1U, 1001U, 2001U));
for (uint32_t i = 0U; i < 20U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).isRunning());
REQUIRE(harness.m_control->getLastSrcId(1U) == 1001U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2001U);
}
TEST_CASE("DMR host e2e loopback handles dropped call terminator and returns idle", "[dmr][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 = 7003U;
const uint32_t streamId = 0x610002U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
DMRTestNetwork sender(senderPort, 7004U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 200U, 1U, 1101U, 2101U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(sender.sendDMRTerminator(hostPeerId, streamId, RTP_END_OF_CALL_SEQ, 1U, 1101U, 2101U));
for (uint32_t i = 0U; i < 50U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE_FALSE(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).isRunning());
}
TEST_CASE("DMR host e2e loopback times out stale call and resets stream state", "[dmr][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 = 7005U;
const uint32_t streamId = 0x610003U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
DMRTestNetwork sender(senderPort, 7006U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 300U, 1U, 1201U, 2201U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.network()->resetDMRCount() == 0U);
HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).clock(
expireTimerTicks(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control))));
HostTestHooks::dmrSlot1(*harness.m_control)->clock();
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE(harness.network()->resetDMRCount() == 1U);
}
TEST_CASE("DMR host e2e loopback preserves a continuing network stream across an RF collision", "[dmr][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 = 7011U;
const uint32_t streamId = 0x610201U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
DMRTestNetwork sender(senderPort, 7012U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
HostTestHooks::dmrSetRFCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1401U, 2401U);
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 600U, 1U, 1501U, 2501U));
for (uint32_t i = 0U; i < 40U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (harness.network()->rxDMRStreamId(1U) == streamId) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamId);
REQUIRE(harness.network()->resetDMRCount() == 0U);
HostTestHooks::dmrClearRFCall(*HostTestHooks::dmrSlot1(*harness.m_control));
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamId, 601U, 1U, 1501U, 2501U));
for (uint32_t i = 0U; i < 40U; i++) {
sender.clock(1U);
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamId);
REQUIRE(harness.network()->resetDMRCount() == 0U);
}
TEST_CASE("DMR host e2e loopback enforces stream lock until active stream terminates", "[dmr][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 = 7007U;
const uint32_t streamA = 0x610101U;
const uint32_t streamB = 0x610102U;
DMRHostHarness harness(true, true, hostPort, hostPeerId);
DMRTestNetwork sender(senderPort, 7008U);
REQUIRE(harness.network() != nullptr);
REQUIRE(harness.network()->activateLoopback("127.0.0.1", senderPort));
REQUIRE(sender.activateLoopback("127.0.0.1", hostPort));
// Start call on stream A.
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamA, 400U, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.m_control->getLastSrcId(1U) == 1301U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2301U);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamA);
// Competing stream B should be ignored while stream A is active and locked.
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamB, 500U, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 30U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.m_control->getLastSrcId(1U) == 1301U);
REQUIRE(harness.m_control->getLastDstId(1U) == 2301U);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamA);
// End stream A; stream lock should be released.
REQUIRE(sender.sendDMRTerminator(hostPeerId, streamA, RTP_END_OF_CALL_SEQ, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 50U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
REQUIRE(harness.network()->rxDMRStreamId(1U) == 0U);
// Now stream B should be admitted after stream A terminates.
REQUIRE(sender.sendDMRVoiceHeader(hostPeerId, streamB, 502U, 1U, 1301U, 2301U));
for (uint32_t i = 0U; i < 40U; i++) {
harness.network()->clock(1U);
harness.m_control->clock();
if (HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(harness.network()->rxDMRStreamId(1U) == streamB);
}
TEST_CASE("DMR slot network voice start arms watchdog on the targeted slot", "[dmr][host][control]")
{
DMRHostHarness harness;
HostTestHooks::dmrStartNetVoiceCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1001U, 2001U);
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_AUDIO);
REQUIRE(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).isRunning());
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot2(*harness.m_control)) == RS_NET_IDLE);
}
TEST_CASE("DMR slot watchdog expiry returns that slot to idle", "[dmr][host][control]")
{
DMRHostHarness harness;
HostTestHooks::dmrStartNetVoiceCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1001U, 2001U);
HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control)).clock(
expireTimerTicks(HostTestHooks::dmrNetworkWatchdog(*HostTestHooks::dmrSlot1(*harness.m_control))));
HostTestHooks::dmrSlot1(*harness.m_control)->clock();
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
}
TEST_CASE("DMR permittedTG only affects the targeted slot", "[dmr][host][control]")
{
DMRHostHarness harness(false);
harness.m_control->permittedTG(2001U, 1U);
REQUIRE(HostTestHooks::dmrPermittedDstId(*HostTestHooks::dmrSlot1(*harness.m_control)) == 2001U);
REQUIRE(HostTestHooks::dmrPermittedDstId(*HostTestHooks::dmrSlot2(*harness.m_control)) == 0U);
}
TEST_CASE("DMR clearRFReject only clears the targeted slot", "[dmr][host][control][rf]")
{
DMRHostHarness harness;
HostTestHooks::dmrSetRFRejected(*HostTestHooks::dmrSlot1(*harness.m_control));
HostTestHooks::dmrSetRFRejected(*HostTestHooks::dmrSlot2(*harness.m_control));
harness.m_control->clearRFReject(1U);
REQUIRE(HostTestHooks::dmrRFState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_RF_LISTENING);
REQUIRE(HostTestHooks::dmrRFState(*HostTestHooks::dmrSlot2(*harness.m_control)) == RS_RF_REJECTED);
}
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::dmrStartNetVoiceCall(*HostTestHooks::dmrSlot1(*harness.m_control), 1001U, 2001U);
REQUIRE(HostTestHooks::dmrNetState(*HostTestHooks::dmrSlot1(*harness.m_control)) == RS_NET_IDLE);
}
TEST_CASE("DMR processFrame accepts a synthetic RF voice call on the targeted slot", "[dmr][host][control][rf]")
{
DMRHostHarness harness;
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());
}

Powered by TurnKey Linux.