correct possible edge case causing the Rx P25 stream ID to become stuck; build and implement a test harness against the host controller paths to validate logic where possible;
parent
26f9f3019d
commit
399992fec9
@ -0,0 +1,206 @@
|
||||
// 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/NetRPC.h"
|
||||
#include "host/modem/Modem.h"
|
||||
#include "modem/port/IModemPort.h"
|
||||
#include "host/HostTestHooks.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Dummy implementation of a modem port for testing purposes.
|
||||
*/
|
||||
class TestModemPort final : public modem::port::IModemPort {
|
||||
public:
|
||||
/**
|
||||
* @brief Finalizes the instance of the TestModemPort class.
|
||||
*/
|
||||
~TestModemPort() 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 {}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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) :
|
||||
m_rpc("127.0.0.1", 1U, 0U, "test", false),
|
||||
m_modem(new TestModemPort(), 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_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, nullptr, false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup,
|
||||
&m_rssiMapper, 60U, false, false, false, false, false);
|
||||
}
|
||||
/**
|
||||
* @brief Finalizes an instance of the DMRHostHarness class.
|
||||
*/
|
||||
~DMRHostHarness()
|
||||
{
|
||||
delete m_control;
|
||||
g_RPC = nullptr;
|
||||
}
|
||||
|
||||
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;
|
||||
dmr::Control* m_control;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,351 @@
|
||||
// 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/HostTestHooks.h"
|
||||
#include "common/dmr/SlotType.h"
|
||||
#include "common/dmr/Sync.h"
|
||||
#include "common/dmr/lc/FullLC.h"
|
||||
#include "common/nxdn/NXDNDefines.h"
|
||||
#include "common/nxdn/NXDNUtils.h"
|
||||
#include "common/nxdn/Sync.h"
|
||||
#include "common/nxdn/channel/FACCH1.h"
|
||||
#include "common/nxdn/channel/LICH.h"
|
||||
#include "common/nxdn/channel/SACCH.h"
|
||||
#include "common/nxdn/lc/RTCH.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#if defined(CATCH2_TEST_COMPILATION)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const uint8_t DMR_CAL_VH_1K_PAYLOAD[dmr::defines::DMR_FRAME_LENGTH_BYTES] = {
|
||||
0x00U, 0x20U, 0x08U, 0x08U, 0x02U, 0x38U, 0x15U, 0x00U, 0x2CU, 0xA0U, 0x14U,
|
||||
0x60U, 0x84U, 0x6DU, 0xFFU, 0x57U, 0xD7U, 0x5DU, 0xF5U, 0xDEU, 0x30U, 0x30U,
|
||||
0x01U, 0x10U, 0x01U, 0x40U, 0x03U, 0xC0U, 0x13U, 0xC1U, 0x1EU, 0x80U, 0x6FU
|
||||
};
|
||||
|
||||
static const uint8_t NXDN_CAL_1K_PAYLOAD[nxdn::defines::NXDN_FRAME_LENGTH_BYTES] = {
|
||||
0xCDU, 0xF5U, 0x9DU, 0x5DU, 0x7CU, 0xFAU, 0x0AU, 0x6EU, 0x8AU, 0x23U, 0x56U, 0xE8U,
|
||||
0x4CU, 0xAAU, 0xDEU, 0x8BU, 0x26U, 0xE4U, 0xF2U, 0x82U, 0x88U,
|
||||
0xC6U, 0x8AU, 0x74U, 0x29U, 0xA4U, 0xECU, 0xD0U, 0x08U, 0x22U,
|
||||
0xCEU, 0xA2U, 0xFCU, 0x01U, 0x8CU, 0xECU, 0xDAU, 0x0AU, 0xA0U,
|
||||
0xEEU, 0x8AU, 0x7EU, 0x2BU, 0x26U, 0xCCU, 0xF8U, 0x8AU, 0x08U
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static Class Members
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/* Gets DMR slot 1 instance. */
|
||||
|
||||
dmr::Slot* HostTestHooks::dmrSlot1(const dmr::Control& control) { return control.m_slot1; }
|
||||
|
||||
/* Gets DMR slot 2 instance. */
|
||||
|
||||
dmr::Slot* HostTestHooks::dmrSlot2(const dmr::Control& control) { return control.m_slot2; }
|
||||
|
||||
/* Gets DMR network state. */
|
||||
|
||||
RPT_NET_STATE HostTestHooks::dmrNetState(const dmr::Slot& slot) { return slot.m_netState; }
|
||||
|
||||
/* Gets DMR RF state. */
|
||||
|
||||
RPT_RF_STATE HostTestHooks::dmrRFState(const dmr::Slot& slot) { return slot.m_rfState; }
|
||||
|
||||
/* Gets DMR permitted destination ID. */
|
||||
|
||||
uint32_t HostTestHooks::dmrPermittedDstId(const dmr::Slot& slot) { return slot.m_permittedDstId; }
|
||||
|
||||
/* Gets DMR network watchdog timer. */
|
||||
|
||||
Timer& HostTestHooks::dmrNetworkWatchdog(dmr::Slot& slot) { return slot.m_networkWatchdog; }
|
||||
|
||||
/* Gets DMR network talkgroup hang timer. */
|
||||
|
||||
Timer& HostTestHooks::dmrNetTGHang(dmr::Slot& slot) { return slot.m_netTGHang; }
|
||||
|
||||
/* Forces DMR slot into active RF call state. */
|
||||
|
||||
void HostTestHooks::dmrSetRFCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId)
|
||||
{
|
||||
slot.m_rfState = RS_RF_AUDIO;
|
||||
slot.m_rfLastSrcId = srcId;
|
||||
slot.m_rfLastDstId = dstId;
|
||||
slot.m_rfTGHang.start();
|
||||
}
|
||||
|
||||
/* Forces DMR slot into RF rejected state. */
|
||||
|
||||
void HostTestHooks::dmrSetRFRejected(dmr::Slot& slot)
|
||||
{
|
||||
slot.m_rfState = RS_RF_REJECTED;
|
||||
}
|
||||
|
||||
/* Injects synthetic DMR network voice call. */
|
||||
|
||||
void HostTestHooks::dmrStartNetVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group)
|
||||
{
|
||||
dmr::data::NetData dmrData;
|
||||
dmrData.setSlotNo(slot.m_slotNo);
|
||||
dmrData.setSrcId(srcId);
|
||||
dmrData.setDstId(dstId);
|
||||
dmrData.setFLCO(group ? dmr::defines::FLCO::GROUP : dmr::defines::FLCO::PRIVATE);
|
||||
dmrData.setDataType(dmr::defines::DataType::VOICE_LC_HEADER);
|
||||
|
||||
uint8_t data[dmr::defines::DMR_FRAME_LENGTH_BYTES];
|
||||
::memset(data, 0x00U, sizeof(data));
|
||||
|
||||
dmr::lc::LC lc(dmrData.getFLCO(), srcId, dstId);
|
||||
dmr::lc::FullLC fullLC;
|
||||
fullLC.encode(lc, data, dmr::defines::DataType::VOICE_LC_HEADER);
|
||||
|
||||
dmr::SlotType slotType;
|
||||
slotType.setColorCode(dmr::Slot::s_colorCode);
|
||||
slotType.setDataType(dmr::defines::DataType::VOICE_LC_HEADER);
|
||||
slotType.encode(data);
|
||||
|
||||
dmr::Sync::addDMRDataSync(data, dmr::Slot::s_duplex);
|
||||
|
||||
dmrData.setData(data);
|
||||
slot.processNetwork(dmrData);
|
||||
}
|
||||
|
||||
/* Injects synthetic DMR RF voice call frame. */
|
||||
|
||||
bool HostTestHooks::dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group)
|
||||
{
|
||||
uint8_t frame[dmr::defines::DMR_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(frame, 0x00U, sizeof(frame));
|
||||
|
||||
::memcpy(frame + 2U, DMR_CAL_VH_1K_PAYLOAD, dmr::defines::DMR_FRAME_LENGTH_BYTES);
|
||||
|
||||
frame[0U] = modem::TAG_DATA;
|
||||
frame[1U] = dmr::defines::SYNC_DATA | dmr::defines::DataType::VOICE_LC_HEADER;
|
||||
|
||||
dmr::lc::LC lc(group ? dmr::defines::FLCO::GROUP : dmr::defines::FLCO::PRIVATE, srcId, dstId);
|
||||
dmr::lc::FullLC fullLC;
|
||||
fullLC.encode(lc, frame + 2U, dmr::defines::DataType::VOICE_LC_HEADER);
|
||||
|
||||
dmr::SlotType slotType;
|
||||
slotType.setColorCode(dmr::Slot::s_colorCode);
|
||||
slotType.setDataType(dmr::defines::DataType::VOICE_LC_HEADER);
|
||||
slotType.encode(frame + 2U);
|
||||
|
||||
dmr::Sync::addDMRDataSync(frame + 2U, dmr::Slot::s_duplex);
|
||||
|
||||
return slot.processFrame(frame, sizeof(frame));
|
||||
}
|
||||
|
||||
/* Gets P25 network state. */
|
||||
|
||||
RPT_NET_STATE HostTestHooks::p25NetState(const p25::Control& control) { return control.m_netState; }
|
||||
|
||||
/* Gets P25 RF state. */
|
||||
|
||||
RPT_RF_STATE HostTestHooks::p25RFState(const p25::Control& control) { return control.m_rfState; }
|
||||
|
||||
/* Gets P25 last network destination ID. */
|
||||
|
||||
uint32_t HostTestHooks::p25NetLastDstId(const p25::Control& control) { return control.m_netLastDstId; }
|
||||
|
||||
/* Gets P25 last network source ID. */
|
||||
|
||||
uint32_t HostTestHooks::p25NetLastSrcId(const p25::Control& control) { return control.m_netLastSrcId; }
|
||||
|
||||
/* Gets P25 permitted destination ID. */
|
||||
|
||||
uint32_t HostTestHooks::p25PermittedDstId(const p25::Control& control) { return control.m_permittedDstId; }
|
||||
|
||||
/* Gets P25 tail-on-idle flag. */
|
||||
|
||||
bool HostTestHooks::p25TailOnIdle(const p25::Control& control) { return control.m_tailOnIdle; }
|
||||
|
||||
/* Gets P25 network watchdog timer. */
|
||||
|
||||
Timer& HostTestHooks::p25NetworkWatchdog(p25::Control& control) { return control.m_networkWatchdog; }
|
||||
|
||||
/* Gets P25 network talkgroup hang timer. */
|
||||
|
||||
Timer& HostTestHooks::p25NetTGHang(p25::Control& control) { return control.m_netTGHang; }
|
||||
|
||||
/* Forces P25 control into active RF call state. */
|
||||
|
||||
void HostTestHooks::p25SetRFCall(p25::Control& control, uint32_t srcId, uint32_t dstId)
|
||||
{
|
||||
control.m_rfState = RS_RF_AUDIO;
|
||||
control.m_rfLastSrcId = srcId;
|
||||
control.m_rfLastDstId = dstId;
|
||||
control.m_rfTGHang.start();
|
||||
}
|
||||
|
||||
/* Forces P25 control into RF rejected state. */
|
||||
|
||||
void HostTestHooks::p25SetRFRejected(p25::Control& control)
|
||||
{
|
||||
control.m_rfState = RS_RF_REJECTED;
|
||||
}
|
||||
|
||||
/* Encodes and stamps a valid NID for a DUID into a P25 RF frame. */
|
||||
|
||||
void HostTestHooks::p25StampRFFrameNID(p25::Control& control, uint8_t* frame, p25::defines::DUID::E duid)
|
||||
{
|
||||
control.m_nid.encode(frame + 2U, duid);
|
||||
}
|
||||
|
||||
/* Injects synthetic P25 network call start. */
|
||||
|
||||
void HostTestHooks::p25StartNetCall(p25::Control& control, const p25::lc::LC& lc, const p25::data::LowSpeedData& lsd)
|
||||
{
|
||||
p25::packet::Voice* voice = control.m_voice;
|
||||
voice->m_dfsiLC = p25::dfsi::LC(lc, lsd);
|
||||
voice->m_netLastLDU1 = lc;
|
||||
voice->writeNet_LDU1();
|
||||
}
|
||||
|
||||
/* Injects synthetic P25 network call termination. */
|
||||
|
||||
bool HostTestHooks::p25TerminateNetCall(p25::Control& control, const p25::lc::LC& lc, p25::defines::DUID::E duid)
|
||||
{
|
||||
p25::packet::Voice* voice = control.m_voice;
|
||||
uint8_t buffer[1U] = { 0x00U };
|
||||
p25::data::LowSpeedData lsd;
|
||||
p25::defines::FrameType::E frameType = p25::defines::FrameType::DATA_UNIT;
|
||||
p25::lc::LC localControl = lc;
|
||||
p25::defines::DUID::E localDuid = duid;
|
||||
return voice->processNetwork(buffer, 1U, localControl, lsd, localDuid, frameType);
|
||||
}
|
||||
|
||||
/* Gets NXDN network state. */
|
||||
|
||||
RPT_NET_STATE HostTestHooks::nxdnNetState(const nxdn::Control& control) { return control.m_netState; }
|
||||
|
||||
/* Gets NXDN RF state. */
|
||||
|
||||
RPT_RF_STATE HostTestHooks::nxdnRFState(const nxdn::Control& control) { return control.m_rfState; }
|
||||
|
||||
/* Gets NXDN last network destination ID. */
|
||||
|
||||
uint32_t HostTestHooks::nxdnNetLastDstId(const nxdn::Control& control) { return control.m_netLastDstId; }
|
||||
|
||||
/* Gets NXDN last network source ID. */
|
||||
|
||||
uint32_t HostTestHooks::nxdnNetLastSrcId(const nxdn::Control& control) { return control.m_netLastSrcId; }
|
||||
|
||||
/* Gets NXDN permitted destination ID. */
|
||||
|
||||
uint32_t HostTestHooks::nxdnPermittedDstId(const nxdn::Control& control) { return control.m_permittedDstId; }
|
||||
|
||||
/* Gets NXDN network watchdog timer. */
|
||||
|
||||
Timer& HostTestHooks::nxdnNetworkWatchdog(nxdn::Control& control) { return control.m_networkWatchdog; }
|
||||
|
||||
/* Gets NXDN network talkgroup hang timer. */
|
||||
|
||||
Timer& HostTestHooks::nxdnNetTGHang(nxdn::Control& control) { return control.m_netTGHang; }
|
||||
|
||||
/* Forces NXDN control into active RF call state. */
|
||||
|
||||
void HostTestHooks::nxdnSetRFCall(nxdn::Control& control, uint32_t srcId, uint32_t dstId)
|
||||
{
|
||||
control.m_rfState = RS_RF_AUDIO;
|
||||
control.m_rfLastSrcId = srcId;
|
||||
control.m_rfLastDstId = dstId;
|
||||
control.m_rfTGHang.start();
|
||||
}
|
||||
|
||||
/* Forces NXDN control into RF rejected state. */
|
||||
|
||||
void HostTestHooks::nxdnSetRFRejected(nxdn::Control& control)
|
||||
{
|
||||
control.m_rfState = RS_RF_REJECTED;
|
||||
}
|
||||
|
||||
/* Injects synthetic NXDN RF call start frame. */
|
||||
|
||||
bool HostTestHooks::nxdnStartRFCall(nxdn::Control& control, uint32_t srcId, uint32_t dstId)
|
||||
{
|
||||
uint8_t start[nxdn::defines::NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(start, 0x00U, sizeof(start));
|
||||
|
||||
::memcpy(start + 2U, NXDN_CAL_1K_PAYLOAD, nxdn::defines::NXDN_FRAME_LENGTH_BYTES);
|
||||
|
||||
start[0U] = modem::TAG_DATA;
|
||||
start[1U] = 0x01U;
|
||||
|
||||
nxdn::Sync::addNXDNSync(start + 2U);
|
||||
|
||||
nxdn::channel::LICH lich;
|
||||
lich.setRFCT(nxdn::defines::RFChannelType::RTCH);
|
||||
lich.setFCT(nxdn::defines::FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(nxdn::defines::ChOption::STEAL_FACCH);
|
||||
lich.setOutbound(false);
|
||||
lich.encode(start + 2U);
|
||||
|
||||
nxdn::channel::SACCH sacch;
|
||||
sacch.setData(nxdn::defines::SACCH_IDLE);
|
||||
sacch.setRAN(control.m_ran);
|
||||
sacch.setStructure(nxdn::defines::ChStructure::SR_SINGLE);
|
||||
sacch.encode(start + 2U);
|
||||
|
||||
uint8_t buffer[nxdn::defines::NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
nxdn::lc::RTCH lc;
|
||||
lc.setMessageType(nxdn::defines::MessageType::RTCH_VCALL);
|
||||
lc.setSrcId(srcId);
|
||||
lc.setDstId(dstId);
|
||||
lc.setGroup(true);
|
||||
lc.setTransmissionMode(nxdn::defines::TransmissionMode::MODE_4800);
|
||||
lc.encode(buffer, nxdn::defines::NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
nxdn::channel::FACCH1 facch;
|
||||
facch.setData(buffer);
|
||||
facch.encode(start + 2U, nxdn::defines::NXDN_FSW_LENGTH_BITS + nxdn::defines::NXDN_LICH_LENGTH_BITS + nxdn::defines::NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
facch.encode(start + 2U, nxdn::defines::NXDN_FSW_LENGTH_BITS + nxdn::defines::NXDN_LICH_LENGTH_BITS + nxdn::defines::NXDN_SACCH_FEC_LENGTH_BITS + nxdn::defines::NXDN_FACCH1_FEC_LENGTH_BITS);
|
||||
|
||||
nxdn::NXDNUtils::scrambler(start + 2U);
|
||||
|
||||
return control.processFrame(start, sizeof(start));
|
||||
}
|
||||
|
||||
/* Injects synthetic NXDN network call start frame. */
|
||||
|
||||
bool HostTestHooks::nxdnStartNetCall(nxdn::Control& control, const nxdn::lc::RTCH& lc)
|
||||
{
|
||||
uint8_t start[nxdn::defines::NXDN_FRAME_LENGTH_BYTES + 2U];
|
||||
::memset(start, 0x00U, sizeof(start));
|
||||
|
||||
nxdn::Sync::addNXDNSync(start + 2U);
|
||||
|
||||
nxdn::channel::LICH lich;
|
||||
lich.setRFCT(nxdn::defines::RFChannelType::RCCH);
|
||||
lich.setFCT(nxdn::defines::FuncChannelType::USC_SACCH_NS);
|
||||
lich.setOption(nxdn::defines::ChOption::STEAL_FACCH);
|
||||
lich.setOutbound(true);
|
||||
lich.encode(start + 2U);
|
||||
|
||||
nxdn::channel::SACCH sacch;
|
||||
sacch.setData(nxdn::defines::SACCH_IDLE);
|
||||
sacch.setRAN(control.m_ran);
|
||||
sacch.setStructure(nxdn::defines::ChStructure::SR_SINGLE);
|
||||
sacch.encode(start + 2U);
|
||||
|
||||
uint8_t buffer[nxdn::defines::NXDN_RTCH_LC_LENGTH_BYTES];
|
||||
nxdn::lc::RTCH localControl = lc;
|
||||
localControl.encode(buffer, nxdn::defines::NXDN_RTCH_LC_LENGTH_BITS);
|
||||
|
||||
nxdn::channel::FACCH1 facch;
|
||||
facch.setData(buffer);
|
||||
facch.encode(start + 2U, nxdn::defines::NXDN_FSW_LENGTH_BITS + nxdn::defines::NXDN_LICH_LENGTH_BITS + nxdn::defines::NXDN_SACCH_FEC_LENGTH_BITS);
|
||||
facch.encode(start + 2U, nxdn::defines::NXDN_FSW_LENGTH_BITS + nxdn::defines::NXDN_LICH_LENGTH_BITS + nxdn::defines::NXDN_SACCH_FEC_LENGTH_BITS + nxdn::defines::NXDN_FACCH1_FEC_LENGTH_BITS);
|
||||
|
||||
return control.m_voice->processNetwork(nxdn::defines::FuncChannelType::USC_SACCH_NS, nxdn::defines::ChOption::STEAL_FACCH, localControl, start, sizeof(start));
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,271 @@
|
||||
// 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
|
||||
*
|
||||
*/
|
||||
#if !defined(__HOST_TEST_ACCESS_H__)
|
||||
#define __HOST_TEST_ACCESS_H__
|
||||
|
||||
#include "host/dmr/Control.h"
|
||||
#include "host/dmr/Slot.h"
|
||||
#include "host/nxdn/Control.h"
|
||||
#include "host/p25/Control.h"
|
||||
#include "host/p25/packet/Voice.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Provides test-only access to internal members of host protocol control classes.
|
||||
* This class centralizes friend-based accessors and utility helpers used by
|
||||
* Catch2 host tests for DMR, P25, and NXDN.
|
||||
*/
|
||||
class HostTestHooks {
|
||||
public:
|
||||
/**
|
||||
* @name Digital Mobile Radio (DMR)
|
||||
* @brief DMR accessors and RF/network call setup helpers.
|
||||
*/
|
||||
/**
|
||||
* @brief Gets slot 1 instance from the DMR control object.
|
||||
* @param control DMR control instance.
|
||||
* @return dmr::Slot* Pointer to slot 1.
|
||||
*/
|
||||
static dmr::Slot* dmrSlot1(const dmr::Control& control);
|
||||
/**
|
||||
* @brief Gets slot 2 instance from the DMR control object.
|
||||
* @param control DMR control instance.
|
||||
* @return dmr::Slot* Pointer to slot 2.
|
||||
*/
|
||||
static dmr::Slot* dmrSlot2(const dmr::Control& control);
|
||||
/**
|
||||
* @brief Gets the current network state for a DMR slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @return RPT_NET_STATE Current network state.
|
||||
*/
|
||||
static RPT_NET_STATE dmrNetState(const dmr::Slot& slot);
|
||||
/**
|
||||
* @brief Gets the current RF state for a DMR slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @return RPT_RF_STATE Current RF state.
|
||||
*/
|
||||
static RPT_RF_STATE dmrRFState(const dmr::Slot& slot);
|
||||
/**
|
||||
* @brief Gets the currently permitted destination ID for a DMR slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @return uint32_t Permitted destination ID.
|
||||
*/
|
||||
static uint32_t dmrPermittedDstId(const dmr::Slot& slot);
|
||||
/**
|
||||
* @brief Gets the DMR network watchdog timer for a slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @return Timer& Reference to the network watchdog timer.
|
||||
*/
|
||||
static Timer& dmrNetworkWatchdog(dmr::Slot& slot);
|
||||
/**
|
||||
* @brief Gets the DMR network talkgroup hang timer for a slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @return Timer& Reference to the network TG hang timer.
|
||||
*/
|
||||
static Timer& dmrNetTGHang(dmr::Slot& slot);
|
||||
/**
|
||||
* @brief Forces a DMR slot into active RF call state for tests.
|
||||
* @param slot DMR slot instance.
|
||||
* @param srcId Source ID to apply to slot state.
|
||||
* @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 RF rejected state for tests.
|
||||
* @param slot DMR slot instance.
|
||||
*/
|
||||
static void dmrSetRFRejected(dmr::Slot& slot);
|
||||
/**
|
||||
* @brief Injects a synthetic DMR network voice call into a slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @param srcId Source ID for synthetic call.
|
||||
* @param dstId Destination ID for synthetic call.
|
||||
* @param group True for group call, false for private call.
|
||||
*/
|
||||
static void dmrStartNetVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group = true);
|
||||
/**
|
||||
* @brief Injects a synthetic DMR RF voice call frame into a slot.
|
||||
* @param slot DMR slot instance.
|
||||
* @param srcId Source ID for synthetic call.
|
||||
* @param dstId Destination ID for synthetic call.
|
||||
* @param group True for group call, false for private call.
|
||||
* @return bool True if frame processing succeeds.
|
||||
*/
|
||||
static bool dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group = true);
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @name Project 25 (P25)
|
||||
* @brief P25 accessors and RF/network call setup helpers.
|
||||
*/
|
||||
/**
|
||||
* @brief Gets the current network state for P25 control.
|
||||
* @param control P25 control instance.
|
||||
* @return RPT_NET_STATE Current network state.
|
||||
*/
|
||||
static RPT_NET_STATE p25NetState(const p25::Control& control);
|
||||
/**
|
||||
* @brief Gets the current RF state for P25 control.
|
||||
* @param control P25 control instance.
|
||||
* @return RPT_RF_STATE Current RF state.
|
||||
*/
|
||||
static RPT_RF_STATE p25RFState(const p25::Control& control);
|
||||
/**
|
||||
* @brief Gets the last P25 network destination ID.
|
||||
* @param control P25 control instance.
|
||||
* @return uint32_t Last network destination ID.
|
||||
*/
|
||||
static uint32_t p25NetLastDstId(const p25::Control& control);
|
||||
/**
|
||||
* @brief Gets the last P25 network source ID.
|
||||
* @param control P25 control instance.
|
||||
* @return uint32_t Last network source ID.
|
||||
*/
|
||||
static uint32_t p25NetLastSrcId(const p25::Control& control);
|
||||
/**
|
||||
* @brief Gets the currently permitted P25 destination ID.
|
||||
* @param control P25 control instance.
|
||||
* @return uint32_t Permitted destination ID.
|
||||
*/
|
||||
static uint32_t p25PermittedDstId(const p25::Control& control);
|
||||
/**
|
||||
* @brief Gets whether P25 tail is currently active on idle transition.
|
||||
* @param control P25 control instance.
|
||||
* @return bool True if tail-on-idle is set.
|
||||
*/
|
||||
static bool p25TailOnIdle(const p25::Control& control);
|
||||
/**
|
||||
* @brief Gets the P25 network watchdog timer.
|
||||
* @param control P25 control instance.
|
||||
* @return Timer& Reference to the network watchdog timer.
|
||||
*/
|
||||
static Timer& p25NetworkWatchdog(p25::Control& control);
|
||||
/**
|
||||
* @brief Gets the P25 network talkgroup hang timer.
|
||||
* @param control P25 control instance.
|
||||
* @return Timer& Reference to the network TG hang timer.
|
||||
*/
|
||||
static Timer& p25NetTGHang(p25::Control& control);
|
||||
/**
|
||||
* @brief Forces P25 control into active RF call state for tests.
|
||||
* @param control P25 control instance.
|
||||
* @param srcId Source ID to apply to control state.
|
||||
* @param dstId Destination ID to apply to control state.
|
||||
*/
|
||||
static void p25SetRFCall(p25::Control& control, uint32_t srcId, uint32_t dstId);
|
||||
/**
|
||||
* @brief Forces P25 control into RF rejected state for tests.
|
||||
* @param control P25 control instance.
|
||||
*/
|
||||
static void p25SetRFRejected(p25::Control& control);
|
||||
/**
|
||||
* @brief Encodes a valid P25 NID for a given DUID into a test RF frame.
|
||||
* @param control P25 control instance.
|
||||
* @param frame RF frame buffer (tag and data) to update.
|
||||
* @param duid DUID to encode into frame NID bits.
|
||||
*/
|
||||
static void p25StampRFFrameNID(p25::Control& control, uint8_t* frame, p25::defines::DUID::E duid);
|
||||
/**
|
||||
* @brief Injects a synthetic P25 network call start via voice packet path.
|
||||
* @param control P25 control instance.
|
||||
* @param lc Link control data for synthetic call.
|
||||
* @param lsd Low speed data for synthetic call.
|
||||
*/
|
||||
static void p25StartNetCall(p25::Control& control, const p25::lc::LC& lc, const p25::data::LowSpeedData& lsd);
|
||||
/**
|
||||
* @brief Injects synthetic P25 network call termination via voice packet path.
|
||||
* @param control P25 control instance.
|
||||
* @param lc Link control data for synthetic termination.
|
||||
* @param duid DUID to use for termination frame.
|
||||
* @return bool True if termination processing succeeds.
|
||||
*/
|
||||
static bool p25TerminateNetCall(p25::Control& control, const p25::lc::LC& lc, p25::defines::DUID::E duid = p25::defines::DUID::TDU);
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @name NXDN
|
||||
* @brief NXDN accessors and RF/network call setup helpers.
|
||||
*/
|
||||
/**
|
||||
* @brief Gets the current network state for NXDN control.
|
||||
* @param control NXDN control instance.
|
||||
* @return RPT_NET_STATE Current network state.
|
||||
*/
|
||||
static RPT_NET_STATE nxdnNetState(const nxdn::Control& control);
|
||||
/**
|
||||
* @brief Gets the current RF state for NXDN control.
|
||||
* @param control NXDN control instance.
|
||||
* @return RPT_RF_STATE Current RF state.
|
||||
*/
|
||||
static RPT_RF_STATE nxdnRFState(const nxdn::Control& control);
|
||||
/**
|
||||
* @brief Gets the last NXDN network destination ID.
|
||||
* @param control NXDN control instance.
|
||||
* @return uint32_t Last network destination ID.
|
||||
*/
|
||||
static uint32_t nxdnNetLastDstId(const nxdn::Control& control);
|
||||
/**
|
||||
* @brief Gets the last NXDN network source ID.
|
||||
* @param control NXDN control instance.
|
||||
* @return uint32_t Last network source ID.
|
||||
*/
|
||||
static uint32_t nxdnNetLastSrcId(const nxdn::Control& control);
|
||||
/**
|
||||
* @brief Gets the currently permitted NXDN destination ID.
|
||||
* @param control NXDN control instance.
|
||||
* @return uint32_t Permitted destination ID.
|
||||
*/
|
||||
static uint32_t nxdnPermittedDstId(const nxdn::Control& control);
|
||||
/**
|
||||
* @brief Gets the NXDN network watchdog timer.
|
||||
* @param control NXDN control instance.
|
||||
* @return Timer& Reference to the network watchdog timer.
|
||||
*/
|
||||
static Timer& nxdnNetworkWatchdog(nxdn::Control& control);
|
||||
/**
|
||||
* @brief Gets the NXDN network talkgroup hang timer.
|
||||
* @param control NXDN control instance.
|
||||
* @return Timer& Reference to the network TG hang timer.
|
||||
*/
|
||||
static Timer& nxdnNetTGHang(nxdn::Control& control);
|
||||
/**
|
||||
* @brief Forces NXDN control into active RF call state for tests.
|
||||
* @param control NXDN control instance.
|
||||
* @param srcId Source ID to apply to control state.
|
||||
* @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 RF rejected state for tests.
|
||||
* @param control NXDN control instance.
|
||||
*/
|
||||
static void nxdnSetRFRejected(nxdn::Control& control);
|
||||
/**
|
||||
* @brief Injects a synthetic NXDN RF call start frame.
|
||||
* @param control NXDN control instance.
|
||||
* @param srcId Source ID for synthetic call.
|
||||
* @param dstId Destination ID for synthetic call.
|
||||
* @return bool True if frame processing succeeds.
|
||||
*/
|
||||
static bool nxdnStartRFCall(nxdn::Control& control, uint32_t srcId, uint32_t dstId);
|
||||
/**
|
||||
* @brief Injects a synthetic NXDN network call start frame.
|
||||
* @param control NXDN control instance.
|
||||
* @param lc Link control data for synthetic call start.
|
||||
* @return bool True if network processing succeeds.
|
||||
*/
|
||||
static bool nxdnStartNetCall(nxdn::Control& control, const nxdn::lc::RTCH& lc);
|
||||
/** @} */
|
||||
};
|
||||
|
||||
#endif // __HOST_TEST_ACCESS_H__
|
||||
@ -0,0 +1,231 @@
|
||||
// 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/NetRPC.h"
|
||||
#include "common/nxdn/NXDNDefines.h"
|
||||
#include "common/nxdn/lc/RTCH.h"
|
||||
#include "host/modem/Modem.h"
|
||||
#include "modem/port/IModemPort.h"
|
||||
#include "host/HostTestHooks.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
extern network::NetRPC* g_RPC;
|
||||
#include "host/nxdn/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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Dummy implementation of a modem port for testing purposes.
|
||||
*/
|
||||
class TestModemPort final : public modem::port::IModemPort {
|
||||
public:
|
||||
/**
|
||||
* @brief Finalizes the instance of the TestModemPort class.
|
||||
*/
|
||||
~TestModemPort() 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 {}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Harness class for testing NXDN host control functionality.
|
||||
*/
|
||||
class NXDNHostHarness {
|
||||
public:
|
||||
/**
|
||||
* @brief Initializes a new instance of the NXDNHostHarness class.
|
||||
* @param authoritative Indicates whether the host is authoritative.
|
||||
*/
|
||||
explicit NXDNHostHarness(bool authoritative = true) :
|
||||
m_rpc("127.0.0.1", 1U, 0U, "test", false),
|
||||
m_modem(new TestModemPort(), false, false, false, false, false, false,
|
||||
0U, 0U, 0U, 1024U, 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_control(nullptr)
|
||||
{
|
||||
g_RPC = &m_rpc;
|
||||
m_modem.setModeParams(false, false, true);
|
||||
|
||||
m_control = new nxdn::Control(authoritative, 1U, 1U, 4096U, 5U, 2U, &m_modem, nullptr,
|
||||
false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup, &m_rssiMapper, false, false, false);
|
||||
}
|
||||
/**
|
||||
* @brief Finalizes an instance of the NXDNHostHarness class.
|
||||
*/
|
||||
~NXDNHostHarness()
|
||||
{
|
||||
delete m_control;
|
||||
g_RPC = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Starts a network voice call with the specified source and destination IDs.
|
||||
* @param srcId The source ID for the network voice call.
|
||||
* @param dstId The destination ID for the network voice call.
|
||||
*/
|
||||
void startNetworkVoiceCall(uint16_t srcId = 1001U, uint16_t dstId = 2001U)
|
||||
{
|
||||
nxdn::lc::RTCH control;
|
||||
control.setMessageType(nxdn::defines::MessageType::RTCH_VCALL);
|
||||
control.setSrcId(srcId);
|
||||
control.setDstId(dstId);
|
||||
control.setGroup(true);
|
||||
control.setTransmissionMode(nxdn::defines::TransmissionMode::MODE_4800);
|
||||
|
||||
REQUIRE(HostTestHooks::nxdnStartNetCall(*m_control, control));
|
||||
}
|
||||
|
||||
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;
|
||||
nxdn::Control* m_control;
|
||||
};
|
||||
|
||||
TEST_CASE("NXDN host arms the network watchdog when network voice starts", "[nxdn][host][control]")
|
||||
{
|
||||
NXDNHostHarness harness;
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
|
||||
REQUIRE(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).isRunning());
|
||||
REQUIRE_FALSE(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).hasExpired());
|
||||
}
|
||||
|
||||
TEST_CASE("NXDN host watchdog expiry returns network voice to idle", "[nxdn][host][control]")
|
||||
{
|
||||
NXDNHostHarness harness;
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control)));
|
||||
harness.m_control->clock();
|
||||
|
||||
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_IDLE);
|
||||
REQUIRE_FALSE(HostTestHooks::nxdnNetworkWatchdog(*harness.m_control).isRunning());
|
||||
}
|
||||
|
||||
TEST_CASE("NXDN host net hang expiry clears active network voice state", "[nxdn][host][control]")
|
||||
{
|
||||
NXDNHostHarness harness;
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
HostTestHooks::nxdnNetTGHang(*harness.m_control).clock(expireTimerTicks(HostTestHooks::nxdnNetTGHang(*harness.m_control)));
|
||||
harness.m_control->clock();
|
||||
|
||||
REQUIRE(HostTestHooks::nxdnNetState(*harness.m_control) == RS_NET_AUDIO);
|
||||
REQUIRE(HostTestHooks::nxdnNetLastDstId(*harness.m_control) == 0U);
|
||||
REQUIRE(HostTestHooks::nxdnNetLastSrcId(*harness.m_control) == 0U);
|
||||
}
|
||||
|
||||
TEST_CASE("NXDN clearRFReject returns rejected RF state to listening", "[nxdn][host][control][rf]")
|
||||
{
|
||||
NXDNHostHarness harness;
|
||||
HostTestHooks::nxdnSetRFRejected(*harness.m_control);
|
||||
|
||||
harness.m_control->clearRFReject();
|
||||
|
||||
REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_LISTENING);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
nxdn::lc::RTCH control;
|
||||
control.setMessageType(nxdn::defines::MessageType::RTCH_VCALL);
|
||||
control.setSrcId(1001U);
|
||||
control.setDstId(2001U);
|
||||
control.setGroup(true);
|
||||
control.setTransmissionMode(nxdn::defines::TransmissionMode::MODE_4800);
|
||||
|
||||
REQUIRE_FALSE(HostTestHooks::nxdnStartNetCall(*harness.m_control, control));
|
||||
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;
|
||||
|
||||
REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1001U, 2001U));
|
||||
REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO);
|
||||
REQUIRE(harness.m_control->getLastDstId() == 2001U);
|
||||
}
|
||||
@ -0,0 +1,325 @@
|
||||
// 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/NetRPC.h"
|
||||
#include "common/p25/P25Defines.h"
|
||||
#include "common/p25/data/LowSpeedData.h"
|
||||
#include "common/p25/lc/LC.h"
|
||||
#include "host/modem/Modem.h"
|
||||
#include "modem/port/IModemPort.h"
|
||||
#include "host/HostTestHooks.h"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
|
||||
extern network::NetRPC* g_RPC;
|
||||
#include "host/p25/Control.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// LDU1_1K payload bytes from firmware calibration constants (without leading control byte).
|
||||
static const uint8_t CAL_P25_LDU1_1K[p25::defines::P25_LDU_FRAME_LENGTH_BYTES] = {
|
||||
0x55U, 0x75U, 0xF5U, 0xFFU, 0x77U, 0xFFU, 0x29U, 0x35U, 0x54U, 0x7BU, 0xCBU, 0x19U, 0x4DU, 0x0DU, 0xCEU, 0x24U, 0xA1U, 0x24U,
|
||||
0x0DU, 0x43U, 0x3CU, 0x0BU, 0xE1U, 0xB9U, 0x18U, 0x44U, 0xFCU, 0xC1U, 0x62U, 0x96U, 0x27U, 0x60U, 0xE4U, 0xE2U, 0x4AU, 0x10U,
|
||||
0x90U, 0xD4U, 0x33U, 0xC0U, 0xBEU, 0x1BU, 0x91U, 0x84U, 0x4CU, 0xFCU, 0x16U, 0x29U, 0x62U, 0x76U, 0x0EU, 0xC0U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x03U, 0x89U, 0x28U, 0x49U, 0x0DU, 0x43U, 0x3CU, 0x02U, 0xF8U, 0x6EU, 0x46U, 0x11U, 0x3FU, 0xC1U, 0x62U, 0x94U,
|
||||
0x89U, 0xD8U, 0x39U, 0x00U, 0x00U, 0x00U, 0x00U, 0x1CU, 0x38U, 0x24U, 0xA1U, 0x24U, 0x35U, 0x0CU, 0xF0U, 0x2FU, 0x86U, 0xE4U,
|
||||
0x18U, 0x44U, 0xFFU, 0x05U, 0x8AU, 0x58U, 0x9DU, 0x83U, 0xB0U, 0x00U, 0x00U, 0x00U, 0x00U, 0x70U, 0xE2U, 0x4AU, 0x12U, 0x40U,
|
||||
0xD4U, 0x33U, 0xC0U, 0xBEU, 0x1BU, 0x91U, 0x84U, 0x4FU, 0xF0U, 0x16U, 0x29U, 0x62U, 0x76U, 0x0EU, 0x6DU, 0xE5U, 0xD5U, 0x48U,
|
||||
0xADU, 0xE3U, 0x89U, 0x28U, 0x49U, 0x0DU, 0x43U, 0x3CU, 0x08U, 0xF8U, 0x6EU, 0x46U, 0x11U, 0x3FU, 0xC1U, 0x62U, 0x96U, 0x24U,
|
||||
0xD8U, 0x3BU, 0xA1U, 0x41U, 0xC2U, 0xD2U, 0xBAU, 0x38U, 0x90U, 0xA1U, 0x24U, 0x35U, 0x0CU, 0xF0U, 0x2FU, 0x86U, 0xE4U, 0x60U,
|
||||
0x44U, 0xFFU, 0x05U, 0x8AU, 0x58U, 0x9DU, 0x83U, 0x94U, 0xC8U, 0xFBU, 0x02U, 0x35U, 0xA4U, 0xE2U, 0x4AU, 0x12U, 0x43U, 0x50U,
|
||||
0x33U, 0xC0U, 0xBEU, 0x1BU, 0x91U, 0x84U, 0x4FU, 0xF0U, 0x58U, 0x29U, 0x62U, 0x76U, 0x0EU, 0xC0U, 0x00U, 0x00U, 0x00U, 0x0CU,
|
||||
0x89U, 0x28U, 0x49U, 0x0DU, 0x43U, 0x3CU, 0x0BU, 0xE1U, 0xB8U, 0x46U, 0x11U, 0x3FU, 0xC1U, 0x62U, 0x96U, 0x27U, 0x60U, 0xE4U
|
||||
};
|
||||
|
||||
// LDU2_1K payload bytes from firmware calibration constants (without leading control byte).
|
||||
static const uint8_t CAL_P25_LDU2_1K[p25::defines::P25_LDU_FRAME_LENGTH_BYTES] = {
|
||||
0x55U, 0x75U, 0xF5U, 0xFFU, 0x77U, 0xFFU, 0x29U, 0x3AU, 0xB8U, 0xA4U, 0xEFU, 0xB0U, 0x9AU, 0x8AU, 0xCEU, 0x24U, 0xA1U, 0x24U,
|
||||
0x0DU, 0x43U, 0x3CU, 0x0BU, 0xE1U, 0xB9U, 0x18U, 0x44U, 0xFCU, 0xC1U, 0x62U, 0x96U, 0x27U, 0x60U, 0xECU, 0xE2U, 0x4AU, 0x10U,
|
||||
0x90U, 0xD4U, 0x33U, 0xC0U, 0xBEU, 0x1BU, 0x91U, 0x84U, 0x4CU, 0xFCU, 0x16U, 0x29U, 0x62U, 0x76U, 0x0EU, 0x40U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x03U, 0x89U, 0x28U, 0x49U, 0x0DU, 0x43U, 0x3CU, 0x02U, 0xF8U, 0x6EU, 0x46U, 0x11U, 0x3FU, 0xC1U, 0x62U, 0x94U,
|
||||
0x89U, 0xD8U, 0x3BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x38U, 0x24U, 0xA1U, 0x24U, 0x35U, 0x0CU, 0xF0U, 0x2FU, 0x86U, 0xE4U,
|
||||
0x18U, 0x44U, 0xFFU, 0x05U, 0x8AU, 0x58U, 0x9DU, 0x83U, 0x90U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0xE2U, 0x4AU, 0x12U, 0x40U,
|
||||
0xD4U, 0x33U, 0xC0U, 0xBEU, 0x1BU, 0x91U, 0x84U, 0x4FU, 0xF0U, 0x16U, 0x29U, 0x62U, 0x76U, 0x0EU, 0xE0U, 0xE0U, 0x00U, 0x00U,
|
||||
0x00U, 0x03U, 0x89U, 0x28U, 0x49U, 0x0DU, 0x43U, 0x3CU, 0x08U, 0xF8U, 0x6EU, 0x46U, 0x11U, 0x3FU, 0xC1U, 0x62U, 0x96U, 0x24U,
|
||||
0xD8U, 0x39U, 0xAEU, 0x8BU, 0x48U, 0xB6U, 0x49U, 0x38U, 0x90U, 0xA1U, 0x24U, 0x35U, 0x0CU, 0xF0U, 0x2FU, 0x86U, 0xE4U, 0x60U,
|
||||
0x44U, 0xFFU, 0x05U, 0x8AU, 0x58U, 0x9DU, 0x83U, 0xB9U, 0xA8U, 0xF4U, 0xF1U, 0xFDU, 0x60U, 0xE2U, 0x4AU, 0x12U, 0x43U, 0x50U,
|
||||
0x33U, 0xC0U, 0xBEU, 0x1BU, 0x91U, 0x84U, 0x4FU, 0xF0U, 0x58U, 0x29U, 0x62U, 0x76U, 0x0EU, 0x40U, 0x00U, 0x00U, 0x00U, 0x0CU,
|
||||
0x89U, 0x28U, 0x49U, 0x0DU, 0x43U, 0x3CU, 0x0BU, 0xE1U, 0xB8U, 0x46U, 0x11U, 0x3FU, 0xC1U, 0x62U, 0x96U, 0x27U, 0x60U, 0xECU
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 Builds a P25 RF frame.
|
||||
* @param payload The payload data to include in the frame.
|
||||
* @param frame The buffer to store the constructed frame.
|
||||
*/
|
||||
void buildP25RFFrame(const uint8_t* payload, uint8_t* frame)
|
||||
{
|
||||
frame[0U] = modem::TAG_DATA;
|
||||
frame[1U] = 0x01U;
|
||||
::memcpy(frame + 2U, payload, p25::defines::P25_LDU_FRAME_LENGTH_BYTES);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Dummy implementation of a modem port for testing purposes.
|
||||
*/
|
||||
class TestModemPort final : public modem::port::IModemPort {
|
||||
public:
|
||||
/**
|
||||
* @brief Finalizes the instance of the TestModemPort class.
|
||||
*/
|
||||
~TestModemPort() 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 {}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Class Declaration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Harness class for testing P25 host control functionality.
|
||||
*/
|
||||
class P25HostHarness {
|
||||
public:
|
||||
/**
|
||||
* @brief Initializes a new instance of the P25HostHarness class.
|
||||
* @param authoritative Indicates whether the host is authoritative.
|
||||
*/
|
||||
explicit P25HostHarness(bool authoritative = true) :
|
||||
m_rpc("127.0.0.1", 1U, 0U, "test", false),
|
||||
m_modem(new TestModemPort(), false, false, false, false, false, false,
|
||||
0U, 0U, 0U, 1024U, 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_control(nullptr)
|
||||
{
|
||||
g_RPC = &m_rpc;
|
||||
m_modem.setModeParams(false, true, false);
|
||||
|
||||
m_control = new p25::Control(authoritative, 0x293U, 1U, 4096U, &m_modem, nullptr,
|
||||
5U, 2U, false, &m_chLookup, &m_ridLookup, &m_tidLookup, &m_idenLookup,
|
||||
&m_rssiMapper, false, false, false, false, false);
|
||||
}
|
||||
/**
|
||||
* @brief Finalizes an instance of the P25HostHarness class.
|
||||
*/
|
||||
~P25HostHarness()
|
||||
{
|
||||
delete m_control;
|
||||
g_RPC = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Starts a network voice call with the specified source and destination IDs.
|
||||
* @param srcId The source ID for the network voice call.
|
||||
* @param dstId The destination ID for the network voice call.
|
||||
*/
|
||||
void startNetworkVoiceCall(uint32_t srcId = 1001U, uint32_t dstId = 2001U)
|
||||
{
|
||||
p25::lc::LC control;
|
||||
control.setLCO(p25::defines::LCO::GROUP);
|
||||
control.setMFId(p25::defines::MFG_STANDARD);
|
||||
control.setSrcId(srcId);
|
||||
control.setDstId(dstId);
|
||||
|
||||
p25::data::LowSpeedData lsd;
|
||||
|
||||
HostTestHooks::p25StartNetCall(*m_control, control, lsd);
|
||||
}
|
||||
|
||||
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;
|
||||
p25::Control* m_control;
|
||||
};
|
||||
|
||||
TEST_CASE("P25 host arms the network watchdog when network voice starts", "[p25][host][control]")
|
||||
{
|
||||
P25HostHarness harness;
|
||||
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_AUDIO);
|
||||
REQUIRE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
|
||||
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).hasExpired());
|
||||
}
|
||||
|
||||
TEST_CASE("P25 host watchdog expiry returns network voice to idle", "[p25][host][control]")
|
||||
{
|
||||
P25HostHarness harness;
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
HostTestHooks::p25NetworkWatchdog(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetworkWatchdog(*harness.m_control)));
|
||||
harness.m_control->clock();
|
||||
|
||||
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
|
||||
REQUIRE(HostTestHooks::p25TailOnIdle(*harness.m_control));
|
||||
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
|
||||
}
|
||||
|
||||
TEST_CASE("P25 host net hang expiry clears active network voice state", "[p25][host][control]")
|
||||
{
|
||||
P25HostHarness harness;
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
HostTestHooks::p25NetTGHang(*harness.m_control).clock(expireTimerTicks(HostTestHooks::p25NetTGHang(*harness.m_control)));
|
||||
harness.m_control->clock();
|
||||
|
||||
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
|
||||
REQUIRE(HostTestHooks::p25TailOnIdle(*harness.m_control));
|
||||
REQUIRE(HostTestHooks::p25NetLastDstId(*harness.m_control) == 0U);
|
||||
REQUIRE(HostTestHooks::p25NetLastSrcId(*harness.m_control) == 0U);
|
||||
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
|
||||
}
|
||||
|
||||
TEST_CASE("P25 host network terminator clears active network voice state", "[p25][host][control]")
|
||||
{
|
||||
P25HostHarness harness;
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
p25::lc::LC control;
|
||||
control.setLCO(p25::defines::LCO::GROUP);
|
||||
control.setSrcId(1001U);
|
||||
control.setDstId(2001U);
|
||||
|
||||
REQUIRE(HostTestHooks::p25TerminateNetCall(*harness.m_control, control));
|
||||
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
|
||||
REQUIRE(HostTestHooks::p25TailOnIdle(*harness.m_control));
|
||||
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
|
||||
}
|
||||
|
||||
TEST_CASE("P25 non-authoritative unpermit clears active network state", "[p25][host][control]")
|
||||
{
|
||||
P25HostHarness harness(false);
|
||||
harness.m_control->permittedTG(2001U);
|
||||
harness.startNetworkVoiceCall();
|
||||
|
||||
harness.m_control->permittedTG(0U);
|
||||
|
||||
REQUIRE(HostTestHooks::p25PermittedDstId(*harness.m_control) == 0U);
|
||||
REQUIRE(HostTestHooks::p25NetState(*harness.m_control) == RS_NET_IDLE);
|
||||
REQUIRE_FALSE(HostTestHooks::p25NetworkWatchdog(*harness.m_control).isRunning());
|
||||
}
|
||||
|
||||
TEST_CASE("P25 clearRFReject returns rejected RF state to listening", "[p25][host][control][rf]")
|
||||
{
|
||||
P25HostHarness harness;
|
||||
HostTestHooks::p25SetRFRejected(*harness.m_control);
|
||||
|
||||
harness.m_control->clearRFReject();
|
||||
|
||||
REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_LISTENING);
|
||||
}
|
||||
|
||||
TEST_CASE("P25 processFrame accepts calibration-based RF LDU1 frame", "[p25][host][rf]")
|
||||
{
|
||||
P25HostHarness harness;
|
||||
harness.m_control->reset();
|
||||
|
||||
uint8_t frame[p25::defines::P25_LDU_FRAME_LENGTH_BYTES + 2U];
|
||||
buildP25RFFrame(CAL_P25_LDU1_1K, frame);
|
||||
HostTestHooks::p25StampRFFrameNID(*harness.m_control, frame, p25::defines::DUID::LDU1);
|
||||
|
||||
REQUIRE(harness.m_control->processFrame(frame, sizeof(frame)));
|
||||
REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_AUDIO);
|
||||
REQUIRE(harness.m_control->getLastSrcId() == 1U);
|
||||
REQUIRE(harness.m_control->getLastDstId() == 1U);
|
||||
}
|
||||
|
||||
TEST_CASE("P25 processFrame accepts calibration-based RF LDU2 frame after LDU1", "[p25][host][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(harness.m_control->processFrame(ldu2, sizeof(ldu2)));
|
||||
REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_AUDIO);
|
||||
REQUIRE(harness.m_control->getLastSrcId() == 1U);
|
||||
REQUIRE(harness.m_control->getLastDstId() == 1U);
|
||||
}
|
||||
Loading…
Reference in new issue